/*******************************************************************************
***  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 "GoalEntryWidget.h"
using pecunia::currency::Money;
using ::QWidget;
using std::chrono::hours;
using std::set;
using drn::accounting::AccountNumber;
using drn::banking::allSupportedAccountTypes;
using drn::banking::SupportedAccountTypes;
using drn::banking::BankName;
using drn::banking::toAccountTypes;
using drn::budgeting::BudgetSource;
using drn::desktop_ui::internal::GoalEntryWidget;
using drn::foundation::Days;
using drn::foundation::Optional;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
#include <QDate>
using ::QDate;
#include <QDebug>
// using qDebug
#include <QString>
using ::QString;

#include <algorithm>
using std::find_if;
#include <exception>
using std::exception;

#include <accounting/Account.h>
using drn::accounting::AccountTypes;
#include <accounting/AccountCode.h>
using drn::accounting::AccountCode;
#include <banking/BankAccount.h>
using drn::banking::BankAccount;
#include <banking/BankAccountType.h>
using drn::banking::BankAccountType;
#include <budgeting/Algorithm.h>
using drn::budgeting::adjustTo;
#include <budgeting/EventFrequency.h>
using drn::budgeting::EventFrequency;
#include <budgeting/Goal.h>
using drn::budgeting::Goal;
#include <foundation/ObserverPtr.hpp>
using drn::foundation::ObserverPtr;
#include "DisableSignalsTemporarily.hpp"
using drn::desktop_ui::internal::DisableSignalsTemporarily;


namespace
{

set<SupportedAccountTypes> goalAccountTypes()
{
	set<SupportedAccountTypes> sats{};

	for (const auto& bankAccountType : allSupportedAccountTypes)
		if (toAccountTypes(bankAccountType) == AccountTypes::Asset)
			sats.insert(bankAccountType);
	return sats;
}

}

void GoalEntryWidget::updatePercentageAmount(const Money& goal, const Money& monthlyGoal)
{
	qDebug() << "Goal Entry Widget New Goal:" << QString::fromStdString(to_string(goal));
	// Disabling the signals prior to setting to prevent triggering an update that would triggers
	// further updates.
	const DisableSignalsTemporarily<MoneyEntryWidget> blockPeriod{this->periodAmount};
	const DisableSignalsTemporarily<QDoubleSpinBox> blockMonthlyWage{this->monthlyWageDblspnbx};
	this->periodAmount->setMoney(goal);
	this->monthlyWageDblspnbx->setValue(
		this->totalWage_ > 0 && monthlyGoal.hasAmount()
			? monthlyGoal / this->totalWage_ * 100.0
			: 0.0
	);
}

void GoalEntryWidget::markDirty()
{
	qDebug() << "Goal Entry Widget is Dirty:" << this->sourceLndt->text();
	this->isDirty_.flipOn();
}

void GoalEntryWidget::updateByPercentage(const double value)
{
	qDebug() << "Goal Entry Widget Updating Based Upon Percentage";

	if (
		this->periodAmount->code() != Iso4217Codes::XXX
		&& this->period() != EventFrequency::Unknown
	)
	{
		const auto percentage{value / 100.0};
		Money monthly{0, 0u, this->periodAmount->code()};
		monthly = percentage * this->totalWage_;
		const auto goal{
			adjustTo(
				this->period(),
				monthly,
				EventFrequency::Monthly,
				this->hrInWeek_,
				this->dayInWeek_
			)
		};
		this->updatePercentageAmount(goal, monthly);
	}
	else
		this->updatePercentageAmount({}, {});
}

void GoalEntryWidget::updateByAmount()
{
	qDebug() << "Goal Entry Widget Updating Based Upon Amount";

	if (
		this->periodAmount->code() != Iso4217Codes::XXX
		&& this->period() != EventFrequency::Unknown
	)
	{
		const auto goal{this->periodAmount->money()};
		const auto monthly{
			adjustTo(
				EventFrequency::Monthly,
				goal,
				this->period(),
				this->hrInWeek_,
				this->dayInWeek_
			)
		};
		this->updatePercentageAmount(goal, monthly);
	}
	else
		this->updatePercentageAmount({}, {});
}

void GoalEntryWidget::updateByPeriod()
{
	qDebug() << "Goal Entry Widget Updating Based Upon Frequency Period";

	if (
		this->periodAmount->code() != Iso4217Codes::XXX
		&& this->period() != EventFrequency::Unknown
	)
	{
		const auto goal{
			adjustTo(
				this->period(),
				this->periodAmount->money(),
				this->previousPeriod_ == EventFrequency::Unknown
					? this->period()
					: this->previousPeriod_,
				this->hrInWeek_,
				this->dayInWeek_
			)
		};
		const auto monthly{
			adjustTo(
				EventFrequency::Monthly,
				goal,
				this->period(),
				this->hrInWeek_,
				this->dayInWeek_
			)
		};
		this->updatePercentageAmount(goal, monthly);
	}
	else
		this->updatePercentageAmount({}, {});
	this->previousPeriod_ = this->frequencyEntry->frequency();
}

void GoalEntryWidget::updateByCurrency()
{
	qDebug() << "Goal Entry Widget Updating Based Upon Currency";

	if (
		this->periodAmount->code() != Iso4217Codes::XXX
		&& this->period() != EventFrequency::Unknown
	)
	{
		const Money previousPeriodAmount{
			this->periodAmount->money().major(),
			this->periodAmount->money().minor(),
			this->previousCode_
		};
		Money newPeriodAmount{0, 0u, this->periodAmount->code()};
		newPeriodAmount = previousPeriodAmount;
		const auto monthly{
			adjustTo(
				EventFrequency::Monthly,
				newPeriodAmount,
				this->period(),
				this->hrInWeek_,
				this->dayInWeek_
			)
		};
		const Money previousFinal{
			this->finalAmount->money().major(),
			this->finalAmount->money().minor(),
			(this->previousCode_ == Iso4217Codes::XXX) ? this->periodAmount->code() : this->previousCode_
		};
		Money newFinalAmount{0, 0u, this->periodAmount->code()};
		newFinalAmount = previousFinal;
		this->finalAmount->setMoney(newFinalAmount);
		this->updatePercentageAmount(newPeriodAmount, monthly);
	}
	else
		this->updatePercentageAmount({}, {});
	this->previousCode_ = this->periodAmount->code();
}

void GoalEntryWidget::onLookUpBankAccounts(const BankName& bn)
{
	this->isBankRequested_ = bn;
	this->markDirty();
	emit lookUpBankAccounts(bn);
}

GoalEntryWidget::GoalEntryWidget(
	const Goal& goal,
	const Optional<BankAccount>& ba,
	const set<BankName>& banks,
	const Money& totalWage,
	const hours& hrInWeek,
	const Days& dayInWeek,
	const set<Iso4217Codes>& usableCurrencies,
	const ObserverPtr<QWidget> parent
) :
	QWidget{parent.observed()},
	isDirty_{},
	original_{goal.source().isEmpty() ? Optional<BudgetSource>{} : goal.source()},
	previousPeriod_{EventFrequency::Unknown},
	previousCode_{Iso4217Codes::XXX},
	totalWage_{totalWage},
	hrInWeek_{hrInWeek},
	dayInWeek_{dayInWeek},
	isBankRequested_{}
{
	this->setupUi(this);

	if (this->original_.hasValue())
	{
		this->sourceLndt->setText(this->original_->name());
		this->sourceLndt->setCursorPosition(0);
		this->nextOccurOnDtdt->setDate(goal.nextOccurOn());
	}
	else
		this->nextOccurOnDtdt->setDate(QDate::currentDate());
	this->periodAmount->setUsableCodes(usableCurrencies);
	this->periodAmount->setMoney(goal.amount());
	this->previousCode_ = goal.amount().code();
	this->frequencyEntry->setFrequency(goal.period());
	this->previousPeriod_ = this->frequencyEntry->frequency();
	this->updateByAmount();
	this->finalAmount->setLabel(tr("Final Total:"));
	this->finalAmount->setUsableCodes(usableCurrencies);
	this->finalAmount->hideCodes();
	this->finalAmount->setMoney(goal.final());
	this->errorMessageLbl->hide();
	this->storedInCmbbx->setBanks(banks);
	this->storedInCmbbx->setSupportedAccountTypes(goalAccountTypes());
	this->storedInCmbbx->disableSupportedAccountTypes();

	if (ba.hasValue())
		this->storedInCmbbx->setBankAccount(*ba);

	//{ Update Error Message Colour

	QPalette error{this->errorMessageLbl->palette()}; // TODO: all error message labels should be promoted to an ErrorLabel type.
	error.setColor(this->errorMessageLbl->foregroundRole(), Qt::red);
	this->errorMessageLbl->setPalette(error);

	//}

	connect(
		this->sourceLndt,
		&QLineEdit::textEdited,
		this,
		&GoalEntryWidget::markDirty
	);

	//{ Connections for periodAmount

	connect(
		this->periodAmount,
		&MoneyEntryWidget::editingFinished,
		this,
		&GoalEntryWidget::markDirty
	);
	connect(
		this->periodAmount,
		&MoneyEntryWidget::editingFinished,
		this,
		&GoalEntryWidget::updateByAmount
	);
	connect(
		this->periodAmount,
		&MoneyEntryWidget::editingFinished,
		this,
		&GoalEntryWidget::valueChanged
	);
	connect(
		this->periodAmount,
		static_cast<void(MoneyEntryWidget::*)(int)>(&MoneyEntryWidget::currentIndexChanged),
		this,
		&GoalEntryWidget::markDirty
	);
	connect(
		this->periodAmount,
		static_cast<void(MoneyEntryWidget::*)(int)>(&MoneyEntryWidget::currentIndexChanged),
		this,
		&GoalEntryWidget::updateByCurrency
	);
	connect(
		this->periodAmount,
		static_cast<void(MoneyEntryWidget::*)(int)>(&MoneyEntryWidget::currentIndexChanged),
		this,
		&GoalEntryWidget::valueChanged
	);

	//}

	//{ Connections for monthlyWageDblspnbx

	connect(
		this->monthlyWageDblspnbx,
		static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
		this,
		&GoalEntryWidget::updateByPercentage
	);
	connect(
		this->monthlyWageDblspnbx,
		static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
		this,
		&GoalEntryWidget::markDirty
	);
	connect(
		this->monthlyWageDblspnbx,
		static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged),
		this,
		&GoalEntryWidget::valueChanged
	);

	//}

	//{ Connection for frequencyEntry

	connect(
		this->frequencyEntry,
		static_cast<void(EventFrequencyEntryWidget::*)(int)>(
			&EventFrequencyEntryWidget::currentIndexChanged
		),
		this,
		&GoalEntryWidget::markDirty
	);
	connect(
		this->frequencyEntry,
		static_cast<void(EventFrequencyEntryWidget::*)(int)>(
			&EventFrequencyEntryWidget::currentIndexChanged
		),
		this,
		&GoalEntryWidget::updateByPeriod
	);
	connect(
		this->frequencyEntry,
		static_cast<void(EventFrequencyEntryWidget::*)(int)>(
			&EventFrequencyEntryWidget::currentIndexChanged
		),
		this,
		&GoalEntryWidget::valueChanged
	);

	//}

	//{ Connection for finalAmount

	connect(
		this->periodAmount,
		static_cast<void(MoneyEntryWidget::*)(int)>(&MoneyEntryWidget::currentIndexChanged),
		this->finalAmount,
		&MoneyEntryWidget::setCurrentIndex
	);
	connect(
		this->finalAmount,
		&MoneyEntryWidget::editingFinished,
		this,
		&GoalEntryWidget::markDirty
	);

	//}

	connect(
		this->removeChckbx,
		&QCheckBox::stateChanged,
		this,
		&GoalEntryWidget::markDirty
	);
	connect(
		this->removeChckbx,
		&QCheckBox::stateChanged,
		this,
		&GoalEntryWidget::valueChanged
	);
	connect(
		this->nextOccurOnDtdt,
		&QDateEdit::dateChanged,
		this,
		&GoalEntryWidget::markDirty
	);
	connect(
		this->storedInCmbbx,
		&BankAccountsWidget::lookUpBankAccounts,
		this,
		&GoalEntryWidget::onLookUpBankAccounts
	);
	connect(
		this->storedInCmbbx,
		&BankAccountsWidget::changedBankName,
		this,
		&GoalEntryWidget::markDirty
	);
	connect(
		this->storedInCmbbx,
		&BankAccountsWidget::changedAccountNumber,
		this,
		&GoalEntryWidget::markDirty
	);
}

const Optional<BudgetSource>& GoalEntryWidget::originalSource() const
{
	return this->original_;
}

BudgetSource GoalEntryWidget::source() const
{
	auto text{this->sourceLndt->text()};

	if (text.isEmpty())
		return {};
	return BudgetSource{std::move(text)};
}

Money GoalEntryWidget::amount() const
{
	return this->periodAmount->money();
}

EventFrequency GoalEntryWidget::period() const
{
	return this->frequencyEntry->frequency();
}

Money GoalEntryWidget::final() const
{
	return this->finalAmount->money();
}

QDate GoalEntryWidget::nextOccurOn() const
{
	return this->nextOccurOnDtdt->date();
}

set<BankName> GoalEntryWidget::banks() const noexcept
{
	return this->storedInCmbbx->banks();
}

Optional<BankName> GoalEntryWidget::bank() const noexcept
{
	return this->storedInCmbbx->bank();
}

Optional<AccountCode> GoalEntryWidget::account() const noexcept
{
	return this->storedInCmbbx->accountCode();
}

Optional<SupportedAccountTypes> GoalEntryWidget::bankAccountType() const noexcept
{
	return this->storedInCmbbx->type();
}

bool GoalEntryWidget::shouldRemove() const
{
	return this->removeChckbx->isChecked();
}

bool GoalEntryWidget::isDirty() const
{
	return this->isDirty_;
}

void GoalEntryWidget::saved()
{
	this->original_ = BudgetSource{this->sourceLndt->text()};
	this->errorMessageLbl->hide();
	this->errorMessageLbl->clear();
	this->isDirty_.flipOff();
}

void GoalEntryWidget::setErrorMessage(const QString& message)
{
	this->errorMessageLbl->setText(message);
	this->errorMessageLbl->show();
	this->isDirty_.flipOn();
}

void GoalEntryWidget::resetDirty()
{
	this->isDirty_.flipOff();
}

void GoalEntryWidget::setAccountCodes(set<BankAccountType> accountTypes)
{
	this->storedInCmbbx->lookedUpBankAccounts(std::move(accountTypes));
	this->isBankRequested_.reset();
}

bool GoalEntryWidget::isRequested(const BankName& bn) const
{
	return this->isBankRequested_ == bn;
}
