/*******************************************************************************
***  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 "ExchangeRatesFile.h"
using drn::file_storage::ExchangeRatesFile;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
using pecunia::currency::toIso4217Code;
using pecunia::currency::toStdString;
#include <QDataStream>
using ::QDataStream;
#include <QDateTime>
using ::QDateTime;
#include <QDebug>
// using qWarning.
#include <QDir>
using ::QDir;
#include <QFile>
using ::QFile;
#include <QFileInfo>
using ::QFileInfo;
#include <QMap>
using ::QMap;
#include <QObject>
using ::QObject;
#include <QStandardPaths>
using ::QStandardPaths;

#include <chrono>
using std::chrono::duration_cast;
using std::chrono::milliseconds;
using std::chrono::system_clock;
#include <exception>
using std::exception;
#include <map>
using std::map;
#include <set>
using std::set;
#include <utility>
using std::move;

#include <drn/config.h>
// using DuxReiNummariae_APPLICATION_SHORT_NAME.
#include <conversion/ConversionMap.h>
using drn::conversion::ConversionMap;
using drn::conversion::ConversionRatio;
#include "FileStorageErrors.h"
using drn::file_storage::BinaryFileError;


// (D)ux (R)ei (N)ummariae (E)xchange-rates.
const quint32 ExchangeRatesFile::magicFileMarker_{0x44524E45};
const quint8 ExchangeRatesFile::latestVersion_{1u};

const QString& ExchangeRatesFile::filePath() const
{
	static const auto filePath{QStandardPaths::writableLocation(QStandardPaths::CacheLocation)};
	return filePath;
}

const QString& ExchangeRatesFile::filePathName() const
{
	static const auto filePathName{this->filePath() + "/exchange-rates.dat"};
	return filePathName;
}

bool ExchangeRatesFile::exists() const
{
	QFileInfo check{this->filePathName()};
	return check.exists() && check.isFile();
}

void ExchangeRatesFile::remove() const
{
	if (this->exists())
	{
		QFile file{this->filePathName()};
		file.remove();
	}
}

ConversionMap ExchangeRatesFile::load() const
{
	QFile file{this->filePathName()};

	if ( ! file.open(QIODevice::ReadOnly))
		throw BinaryFileError{file.fileName(), file.errorString()};
	QDataStream stream(&file);
	quint32 magicNumber;
	quint8 version;
	stream >> magicNumber >> version;

	if (magicNumber != ExchangeRatesFile::magicFileMarker_)
		throw BinaryFileError{
			file.fileName(),
			QObject::tr("Is not a valid %1 exchange-rate file.")
				.arg(DuxReiNummariae_APPLICATION_SHORT_NAME)
		};

	if (version != ExchangeRatesFile::latestVersion_)
		throw BinaryFileError{
			file.fileName(),
			QObject::tr("Contains a version, %1, greater than the latest supported, %2.")
				.arg(version)
				.arg(ExchangeRatesFile::latestVersion_)
		};

	stream.setVersion(QDataStream::Qt_DefaultCompiledVersion);

	try
	{
		QMap<QString, QMap<QString, qreal>> rawConversions;
		QDateTime lastFetched;
		stream >> rawConversions >> lastFetched;
		map<Iso4217Codes, set<ConversionRatio>> ratios;

		for (
			auto fromConversion{rawConversions.constBegin()} ;
			fromConversion != rawConversions.constEnd() ;
			++fromConversion
		)
		{
			const auto from{toIso4217Code(fromConversion.key().toStdString())};

			for (
				auto toConversion{fromConversion.value().constBegin()} ;
				toConversion != fromConversion.value().constEnd() ;
				++toConversion
			)
			{
				const auto to{toIso4217Code(toConversion.key().toStdString())};
				qDebug() << "From:" << fromConversion.key() << "To:" << toConversion.key()
					<< "Ratio:" << toConversion.value();
				ratios[from].emplace(ConversionRatio{to, toConversion.value()});
			}
		}
		return ConversionMap{
			system_clock::time_point{milliseconds{lastFetched.toMSecsSinceEpoch()}},
			move(ratios)
		};
	}
	catch (const exception& error)
	{
		throw BinaryFileError{file.fileName(), QObject::tr("Contains invalid data."), error};
	}
}

void ExchangeRatesFile::store(const ConversionMap& cm) const
{
	QFile file{this->filePathName()};

	if ( ! this->exists())
	{
		QDir d{};

		if ( ! d.mkpath(this->filePath()))
			throw BinaryFileError{
				file.fileName(),
				QObject::tr("Failed to create parent directories.")
			};
	}


	if ( ! file.open(QIODevice::WriteOnly))
		throw BinaryFileError{file.fileName(), file.errorString()};
	QDataStream stream(&file);

	stream << ExchangeRatesFile::magicFileMarker_ << ExchangeRatesFile::latestVersion_;
	stream.setVersion(QDataStream::Qt_DefaultCompiledVersion);

	// Index by the from code on the to code for its ratio.
	QMap<QString, QMap<QString, qreal>> rawConversions;
	for (const auto& fromConversion : cm)
		for (const auto& conversion : fromConversion.second)
			rawConversions[QString::fromStdString(toStdString(fromConversion.first))]
					[QString::fromStdString(toStdString(conversion.convertTo_))]
					= conversion.ratio_;
	stream << rawConversions
			<< QDateTime::fromMSecsSinceEpoch(
				duration_cast<milliseconds>(cm.lastFetched().time_since_epoch()).count()
			);

	if (file.error() != QFileDevice::NoError)
		throw BinaryFileError{file.fileName(), file.errorString()};
}
