/*******************************************************************************
***  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 "MoneyEntryWidget.h"
using pecunia::MinorUnit;
using pecunia::UnitStorage;
using ::QString;
using ::QWidget;
using std::set;
using drn::desktop_ui::MoneyAmountValidator;
using drn::desktop_ui::MoneyEntryWidget;
using drn::foundation::ObserverPtr;
using drn::foundation::SwitchStates;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
using pecunia::currency::minorUnitDigits;
using pecunia::currency::toIso4217Code;
#include <pecunia/Conversion.h>
using pecunia::toMinorUnit;
#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <pecunia/MoneyTypes.h>
using pecunia::MinorUnit;
using pecunia::UnitStorage;
#include <QDebug>
// using qDebug
#include <QLocale>
using ::QLocale;

#include <cmath>
using std::pow;
#include <limits>
using std::numeric_limits;
#include <stdexcept>
using std::exception;
#include <utility>
using std::move;

#include "DesktopUiErrors.h"
using drn::desktop_ui::DataEntryError;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/PresentationText.h>
using drn::foundation::presentationText;
#include <foundation/QtMemory.hpp>
using drn::foundation::makeQtPtr;


namespace
{

const auto emptyCode{presentationText(Iso4217Codes::XXX)}; // TODO: once Code returns an Option remove this and just replace it with an empty text, allowing XXX to be a valid code.
const auto defaultNumberDisplayDigits{2};

struct AmountParts
{
	UnitStorage major_;
	MinorUnit minor_;
	Iso4217Codes code_;
};

AmountParts splitAmount(const QString& amount, const QString& currencyText)
{
	const QLocale locale{};
	const auto currencyIndex{amount.indexOf(locale.decimalPoint())};
	bool majorConverted{false};
	const UnitStorage major{
		locale.toLongLong(amount.left(currencyIndex), &majorConverted)
	};

	if ( ! majorConverted)
		throw DataEntryError{
			QObject::tr("currency"),
			amount,
			QObject::tr("The amount major unit is not a number.")
		};

	bool minorConverted{true};
	const auto minorRaw{
		currencyIndex == -1
			? 0u
			: locale.toUInt(amount.right(amount.size() - currencyIndex - 1), &minorConverted)
	};

	if ( ! minorConverted)
		throw DataEntryError{
			QObject::tr("currency"),
			amount,
			QObject::tr("The amount minor unit is not a number.")
		};

	if (minorRaw > numeric_limits<MinorUnit>::max())
		throw DataEntryError{
			QObject::tr("currency"),
			amount,
			QObject::tr("The amount minor unit is too large.")
		};
	auto minor{static_cast<MinorUnit>(minorRaw)};
	const auto codeStr{currencyText.isEmpty() ? emptyCode : currencyText};

	Iso4217Codes issuerCode;
	try
	{
		issuerCode = toIso4217Code(codeStr.toStdString());
	}
	catch (const exception& error)
	{
		throw DataEntryError{
			QObject::tr("currency"),
			currencyText,
			QObject::tr("The country code is not supported or known."),
			error
		};
	}

	if (minor < pow(10, minorUnitDigits(issuerCode)))
		try
		{
			minor = toMinorUnit(minor, issuerCode);
		}
		catch (const exception& error)
		{
			throw DataEntryError{
				QObject::tr("currency"),
				amount,
				QObject::tr("The minor unit failed conversion."),
				error
			};
		}
	return {major, minor, issuerCode};
}

}

MoneyAmountValidator::MoneyAmountValidator(const QString& code, QObject* parent) :
	QValidator{parent},
	code_{code}
{}

QValidator::State MoneyAmountValidator::validate(QString& input, int& pos) const
try
{
	qDebug() << "Validating" << input << "at" << pos;

	if (input.isEmpty())
		return Intermediate;

	const QLocale locale;
	const auto decimalPoint{locale.decimalPoint()};
	if (input.at(pos > 0 ? pos - 1 : pos) == decimalPoint && input.count(decimalPoint) == 1)
		return Intermediate;
	const auto amount{splitAmount(input, this->code_)};
	const auto& code{amount.code_};
	const auto minimum{Money::minimum(code)};
	const auto& minimumMajor{minimum.first};
	const auto& minimumMinor{minimum.second};
	const auto maximum{Money::maximum(code)};
	const auto& maximumMajor{maximum.first};
	const auto& maximumMinor{maximum.second};

	if (
		(amount.major_ >= minimumMajor)
		&& (amount.minor_ <= minimumMinor)
		&& (amount.major_ <= maximumMajor)
		&& (amount.minor_ <= maximumMinor)
	)
		return Acceptable;
	return Invalid;
}
catch (const exception& error)
{
	qWarning() << "Failure while validating input:" << input << this->code_ << '\n' << error.what();
	input.remove(pos - 1, 1);
	--pos;
	return Invalid;
}

void MoneyEntryWidget::setAmountValidator()
{
	qDebug() << "Adding new money amount validator." << this->codeCmbbx->currentText();
	auto validator{
		makeQtPtr<MoneyAmountValidator>(this->codeCmbbx->currentText(), this)
	};
	this->amountLndt->setValidator(validator.observed());
}

MoneyEntryWidget::MoneyEntryWidget(const ObserverPtr<QWidget>& parent) :
	QWidget{parent.observed()},
	displayDigits_{defaultNumberDisplayDigits},
	isReadOnly_{SwitchStates::Off}
{
	this->setupUi(this);
	this->setUsableCodes({});
	this->setAmountValidator();
	this->codeCmbbx->installEventFilter(this);

	//{ Connect Amount Signals

	connect(
		this->amountLndt,
		&QLineEdit::editingFinished,
		this,
		&MoneyEntryWidget::editingFinished
	);

	connect(
		this->amountLndt,
		&QLineEdit::editingFinished,
		this,
		[this] ()
		{
			try
			{
				const auto amount{this->amountLndt->text()};
				const auto code{this->codeCmbbx->currentText()};

				if (amount.isEmpty())
				{
					qDebug() << "The changed money value results in an empty amount.";
					emit moneyChanged(Money{});
				}
				else
				{
					const auto parts{splitAmount(amount, code)};
					const Money m{parts.major_, parts.minor_, parts.code_};
					emit moneyChanged(m);
				}
			}
			catch (const exception& error)
			{
				qWarning() << "The changed money value does not create a valid money type."
						<< error.what();
				emit moneyChanged(Money{});
			}
		}
	);

	//}

	//{ Connect QComboBox Signals

	connect(
		this->codeCmbbx,
		static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
		this,
		&MoneyEntryWidget::currentIndexChanged
	);
	connect(
		this->codeCmbbx,
		static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
		this,
		&MoneyEntryWidget::setAmountValidator
	);
	connect(
		this->codeCmbbx,
		static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
		this,
		[this] (const int index)
		{
			try
			{
				const auto amount{this->amountLndt->text()};
				const auto code{this->codeCmbbx->itemText(index)};

				if (code.isEmpty() || amount.isEmpty())
				{
					qDebug() << "The changed currency index results in an empty money amount.";
					emit moneyChanged(Money{});
				}
				else
				{
					const auto parts{splitAmount(amount, code)};
					const Money m{parts.major_, parts.minor_, parts.code_};
					emit moneyChanged(m);
				}
			}
			catch (const exception& error)
			{
				qWarning() << "The changed currency index does not create a valid money type."
						<< error.what();
				emit moneyChanged(Money{});
			}
		}
	);

	//}
}

set<Iso4217Codes> MoneyEntryWidget::currencies() const noexcept
{
	set<Iso4217Codes> codes{};

	for (auto index{0}, count{this->codeCmbbx->count()} ; index < count ; ++index)
	{
		const auto text{this->codeCmbbx->itemText(index)};

		if ( ! text.isEmpty())
			codes.emplace(toIso4217Code(text.toStdString()));
	}
	return codes;
}

void MoneyEntryWidget::setUsableCode(const Iso4217Codes& code)
{
	this->setUsableCodes(set<Iso4217Codes>{code});
}

void MoneyEntryWidget::setUsableCodes(const set<Iso4217Codes>& codes)
{
	this->codeCmbbx->clear();

	if (codes.size() == 1)
	{
		this->codeCmbbx->addItem(presentationText(*codes.begin()));
		this->codeCmbbx->setVisible(false);
		this->codeCmbbx->setCurrentIndex(0);
	}
	else
	{
		this->codeCmbbx->addItem("");

		for (const auto& code : codes)
			this->codeCmbbx->addItem(presentationText(code));
		this->codeCmbbx->setVisible(true);
	}
}

void MoneyEntryWidget::setMoney(const Money& money)
{
	qDebug() << "Setting money:" << QString::fromStdString(to_string(money));
	this->amountLndt->setText(presentationText(money));
	this->amountLndt->setCursorPosition(0);
	const auto amountCode{presentationText(money.code())};

	if (this->codeCmbbx->count() > 1)
	{
		this->codeCmbbx->setCurrentIndex(0);

		for (qint32 index{0} ; index < this->codeCmbbx->count() ; ++index)
			if (this->codeCmbbx->itemText(index) == amountCode)
			{
				this->codeCmbbx->setCurrentIndex(index);
				break;
			}
	}
}

Money MoneyEntryWidget::money() const
try
{
	const auto amount{splitAmount(this->amountLndt->text(), this->codeCmbbx->currentText())};
	return Money{amount.major_, amount.minor_, amount.code_};
}
catch (const exception& error)
{
	throw DataEntryError{
		QObject::tr("currency"),
		this->amountLndt->text(),
		QObject::tr("The amount or issued code is not valid."),
		error
	};
}

void MoneyEntryWidget::setLabel(const QString& text)
{
	this->amountLbl->setText(text);
}

void MoneyEntryWidget::hideCodes()
{
	this->codeCmbbx->setVisible(false);
}

void MoneyEntryWidget::showCodes()
{
	this->codeCmbbx->setVisible(true);
}

void MoneyEntryWidget::setCurrentIndex(const int index)
{
	if (index >= this->codeCmbbx->count())
		throw Error{QObject::tr("The currency code index exceeds the number of available codes.")};
	this->codeCmbbx->setCurrentIndex(index);
}

Iso4217Codes MoneyEntryWidget::code() const
{
	const auto amount{splitAmount("0", this->codeCmbbx->currentText())};
	return amount.code_;
}

bool MoneyEntryWidget::eventFilter(QObject* obj, QEvent* event)
{
	if (this->isReadOnly_ && obj == this->codeCmbbx)
	{
		if (
			event->type() == QEvent::MouseButtonPress
			|| event->type() == QEvent::MouseButtonDblClick
			|| event->type() == QEvent::KeyPress
			|| event->type() == QEvent::Wheel
		)
			return true;
	}
	return QWidget::eventFilter(obj, event);
}

void MoneyEntryWidget::setReadOnly(const bool isReadOnly)
{
	if (isReadOnly)
		this->isReadOnly_.flipOn();
	else
		this->isReadOnly_.flipOff();
	this->amountLndt->setReadOnly(this->isReadOnly_);
	this->codeCmbbx->setEnabled( ! this->isReadOnly_);
}

void MoneyEntryWidget::showLabel(const bool shouldShow)
{
	if (shouldShow)
		this->amountLbl->show();
	else
		this->amountLbl->hide();
}

void MoneyEntryWidget::clear()
{
	this->setMoney({});
}

UnitStorage MoneyEntryWidget::major() const
{
	return splitAmount(this->amountLndt->text(), this->codeCmbbx->currentText()).major_;
}

MinorUnit MoneyEntryWidget::minor() const
{
	return splitAmount(this->amountLndt->text(), this->codeCmbbx->currentText()).minor_;
}

QString MoneyEntryWidget::codeText() const
{
	const auto code{this->codeCmbbx->currentText()};
	return code.isEmpty() ? emptyCode : code;
}

bool MoneyEntryWidget::isReadOnly() const
{
	return this->isReadOnly_;
}
