/*
 * Copyright (C) 2014-2024 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <QCoreApplication>
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QFileInfo>
#include <QLibraryInfo>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QTemporaryFile>
#include <QTextStream>

#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/settings/prefs.h"
#include "src/global.h"
#include "src/io/filesystem.h"

/*
 * QRegularExpression::escape() is available since Qt-5.15 therefore we provide
 * the escaped sequences in the code.
 */

/*!
 * @brief Illegal characters in file names without directory separators.
 */
#define ILL_FNAME_CH_NO_SEP ":*?\"<>|"
#define ESCAPED_ILL_FNAME_CH_NO_SEP ":\\*\\?\"<>\\|"
/*!
 * @brief Illegal characters in file name with directory separators.
 */
#define ILL_FNAME_CH "\\/" ILL_FNAME_CH_NO_SEP
#define ESCAPED_ILL_FNAME_CH "\\\\/" ESCAPED_ILL_FNAME_CH_NO_SEP
/*!
 * @brief Replacement character for illegal characters.
 */
#define ILL_FNAME_REP "_"

void replaceIllegalChars(QString &str, bool replaceSeparators)
{
	static const QRegularExpression regExpNoSep(
	    "[" ESCAPED_ILL_FNAME_CH_NO_SEP "]");
	static const QRegularExpression regExp("[" ESCAPED_ILL_FNAME_CH "]");

	str.replace(replaceSeparators ? regExp : regExpNoSep, ILL_FNAME_REP);
}

void replaceBackslashes(QString &str)
{
	static const QRegularExpression regExp("[" "\\\\" "]");

	str.replace(regExp, "/");
}

#undef ILL_FNAME_CH_NO_SEP
#undef ESCAPED_ILL_FNAME_CH_NO_SEP
#undef ILL_FNAME_CH
#undef ESCAPED_ILL_FNAME_CH
#undef ILL_FNAME_REP

bool createDirRecursive(const QString &dirPath)
{
	if (Q_UNLIKELY(dirPath.isEmpty())) {
		return false;
	}

	QDir dir(dirPath);
	if (dir.exists()) {
		return true;
	}

	return dir.mkpath(".");
}

bool createDirStructureRecursive(const QString &filePath)
{
	if (Q_UNLIKELY(filePath.isEmpty())) {
		return false;
	}

	QDir fileDir(QFileInfo(filePath).absoluteDir());
	if (fileDir.exists()) {
		return true;
	}

	return fileDir.mkpath(".");
}

bool copyDirRecursively(const QString &fromDirPath, const QString &toPath,
    bool deleteOnError)
{
	if (Q_UNLIKELY(fromDirPath.isEmpty() || toPath.isEmpty())) {
		return false;
	}

	{
		const QFileInfo fromInfo(fromDirPath);
		if (Q_UNLIKELY(((!fromInfo.isDir()) || (!fromInfo.isReadable())))) {
			/* Not a readable directory. */
			return false;
		}

		const QFileInfo toInfo(toPath);
		if (Q_UNLIKELY((!toInfo.isDir()) || (!toInfo.isWritable()))) {
			/* Not a writeable directory. */
			return false;
		}

		const QString canonicalFrom = fromInfo.canonicalPath();
		const QString canonicalTo = toInfo.canonicalPath();
		if (Q_UNLIKELY(canonicalFrom.isEmpty() || canonicalTo.isEmpty())) {
			Q_ASSERT(0);
			return false;
		}

		if (Q_UNLIKELY(toPath.startsWith(fromDirPath))) {
			/* Cannot copy from into its own subdirectory. */
			return false;
		}
	}

	const QString fromDirName = QFileInfo(fromDirPath).fileName();

	QStringList toBecreatedDirs;
	{
		/* Search for subdirectories to be created. */
		QDirIterator dirIt(fromDirPath, QStringList(),
		    QDir::AllDirs | QDir::NoDotAndDotDot,
		    QDirIterator::Subdirectories);
		while (dirIt.hasNext()) {
			dirIt.next();

			const QString srcDirRelative =
			    QDir(fromDirPath).relativeFilePath(dirIt.filePath());
			const QString tgtDir = toPath + QStringLiteral("/") +
			    fromDirName + QStringLiteral("/") + srcDirRelative;
			{
				const QFileInfo info(tgtDir);
				if (info.exists() && ((!info.isDir()) || (!info.isWritable()))) {
					logErrorNL("The path '%s' is not a writeable directory.",
					    tgtDir.toUtf8().constData());
					return false;
				} else if (!info.exists()) {
					toBecreatedDirs.append(tgtDir);
				}
			}
		}
	}

	/* Create missing subdirectories. */
	for (const QString &toPath : toBecreatedDirs) {
		if (Q_UNLIKELY(!createDirRecursive(toPath))) {
			logErrorNL("Cannot create directory '%s'.",
			    toPath.toUtf8().constData());
			goto fail;
		}
	}

	{
		/* Copy all files. */
		QDirIterator dirIt(fromDirPath, QStringList(),
		    QDir::Files | QDir::NoDotAndDotDot,
		    QDirIterator::Subdirectories);
		while (dirIt.hasNext()) {
			dirIt.next();

			const QString srcFileRelative =
			    QDir(fromDirPath).relativeFilePath(dirIt.filePath());
			const QString tgtFile = toPath + QStringLiteral("/") +
			    fromDirName + QStringLiteral("/") + srcFileRelative;

			QFile::remove(tgtFile); /* Just remove target if already exists. */
			if (Q_UNLIKELY(!QFile::copy(dirIt.filePath(), tgtFile))) {
				logErrorNL("Cannot copy file '%s' to '%s'.",
				    dirIt.filePath().toUtf8().constData(),
				    tgtFile.toUtf8().constData());
				goto fail;
			}
		}
	}

	return true;

fail:
	if (deleteOnError) {
		for (const QString &toPath : toBecreatedDirs) {
			QDir(toPath).removeRecursively();
		}
		/* It doesn't delete newly copied files. */
	}
	return false;
}

QString nonconflictingFileName(QString filePath)
{
	if (filePath.isEmpty()) {
		return QString();
	}

	if (QFile::exists(filePath)) {

		int fileCnt = 0;
		QFileInfo fi(filePath);

		const QString baseName(fi.baseName());
		const QString path(fi.path());
		const QString suffix(fi.completeSuffix());

		do {
			++fileCnt;
			QString newName(
			    baseName + "_" + QString::number(fileCnt));
			filePath =
			    path + QDir::separator() + newName + "." + suffix;
		} while (QFile::exists(filePath));
	}

	return filePath;
}

enum WriteFileState writeFile(const QString &filePath, const QByteArray &data,
    bool deleteOnError)
{
	if (filePath.isEmpty()) {
		Q_ASSERT(0);
		return WF_ERROR;
	}

	QFile fout(filePath);
	if (!fout.open(QIODevice::WriteOnly)) {
		return WF_CANNOT_CREATE;
	}

	int written = fout.write(data);
	bool flushed = fout.flush();
	fout.close();

	if ((written != data.size()) || !flushed) {
		if (deleteOnError) {
			QFile::remove(filePath);
		}
		return WF_CANNOT_WRITE_WHOLE;
	}

	return WF_SUCCESS;
}

/*!
 * @brief Try to determine whether parent holds child.
 *
 * @param[in] parent Parent directory.
 * @param[in] child Child directory.
 * @return True if child lies within or is equal to parent.
 */
static
bool checkDirectoryParent(const QString &parent, const QString &child)
{
	if (Q_UNLIKELY(parent.isEmpty() || child.isEmpty())) {
		return false;
	}

	QDir parentDir(parent);
	QDir childDir(child);
	if (Q_UNLIKELY(!parentDir.exists())) {
		logWarningNL("Directory '%s' does not exist.",
		    parent.toUtf8().constData());
		return false;
	}
	if (Q_UNLIKELY(!childDir.exists())) {
		logWarningNL("Directory '%s' does not exist.",
		    child.toUtf8().constData());
		return false;
	}

	const QString parentCanonical(parentDir.canonicalPath());
	QString childCanonical(childDir.canonicalPath());
	while (parentCanonical != childCanonical) {
		if (!childDir.cdUp()) {
			return false;
		}
		const QString newChildCanonical(childDir.canonicalPath());
		if (childCanonical != newChildCanonical) {
			childCanonical = newChildCanonical;
		} else {
			return false; /* Didn't move. */
		}
	}
	/* Child lies within parent. */
	return true;
}

/*!
 * @brief Return astandard location. If the location could not be aquired then
 *     temporary location is returned.
 *
 * @param[in] location Location identifier.
 * @return Directory path.
 */
static
QString standardDirPath(enum QStandardPaths::StandardLocation location)
{
	QString dirPath(QStandardPaths::writableLocation(location));
	/*
	 * According to Qt documentation GenericCacheLocation may result in
	 * an empty string. Fall back to temptorary directory.
	 */
	if (!dirPath.isEmpty()) {
		if (!checkDirectoryParent(QDir::tempPath(), dirPath)) {
			logWarningNL(
			    "Directory '%s' is likely to lie outside the expected temporary directory '%s'.",
			    dirPath.toUtf8().constData(),
			    QDir::tempPath().toUtf8().constData());
		}
	} else {
		/* QStandardPaths::TempLocation ? */
		dirPath = QDir::tempPath(); /* Should not be empty. */
	}

	return dirPath;
}

QString tempDirPath(bool create, bool share)
{
	QString dirPath;
	{
		bool val = false;
		GlobInstcs::prefsPtr->boolVal(
		    "file_system.temporary.files.store.temp_path.enabled", val);
		if (!val) {
			dirPath = standardDirPath(share ?
			    QStandardPaths::GenericCacheLocation :
			    QStandardPaths::TempLocation);
		} else {
			dirPath = QDir::tempPath();
		}
	}

	if (create) {
		QDir dir(dirPath);
		if ((!dir.exists()) && (!dir.mkdir(QStringLiteral(".")))) {
			logErrorNL(
			    "Directory '%s' does not exist and could not be created.",
			    dirPath.toUtf8().constData());
		}
	}

	return dirPath;
}

QString createTemporarySubdir(const QString &nameTemplate)
{
	if (Q_UNLIKELY(nameTemplate.isEmpty())) {
		Q_ASSERT(0);
		return QString();
	}

	QString nameCopy(nameTemplate);
	replaceIllegalChars(nameCopy, true);

	QTemporaryDir dir(tempDirPath(true, true) + QDir::separator() + nameCopy);
	if (Q_UNLIKELY(!dir.isValid())) {
		logErrorNL("%s", "Could not create a temporary directory.");
		return Q_NULLPTR;
	}
	dir.setAutoRemove(false);

	return dir.path();
}

QString writeTemporaryFile(const QString &fileName, const QByteArray &data,
    bool deleteOnError)
{
	if (fileName.isEmpty()) {
		Q_ASSERT(0);
		return QString();
	}

	QString nameCopy(fileName);
	replaceIllegalChars(nameCopy, true);

	/* StandardLocation::writableLocation(QStandardPaths::TempLocation) ? */
	QTemporaryFile fout(tempDirPath(true, true) + QDir::separator() + nameCopy);
	if (!fout.open()) {
		return QString();
	}
	fout.setAutoRemove(false);

	/* Get whole path. */
	QString fullName = fout.fileName();

	int written = fout.write(data);
	bool flushed = fout.flush();
	fout.close();

	if ((written != data.size()) || !flushed) {
		if (deleteOnError) {
			QFile::remove(fullName);
		}
		return QString();
	}

	return fullName;
}

QString confDirPath(const QString &confSubdir)
{
#define WIN_PREFIX "AppData/Roaming"

#ifdef PORTABLE_APPLICATION
	QString dirPath;

	/* Search in application location. */
	dirPath = QCoreApplication::applicationDirPath();
#  ifdef Q_OS_OSX
	{
		QDir directory(dirPath);
		if ("MacOS" == directory.dirName()) {
			directory.cdUp();
		}
		dirPath = directory.absolutePath() + QDir::separator() +
		    "Resources";
	}
#  endif /* Q_OS_OSX */

	dirPath += QDir::separator() + confSubdir;
	return dirPath;

#else /* !PORTABLE_APPLICATION */
	/* StandardLocation::writableLocation(QStandardPaths::HomeLocation) ? */
	QDir homeDir(QDir::homePath());

#  if defined(Q_OS_WIN)
	if (homeDir.exists(WIN_PREFIX) && !homeDir.exists(confSubdir)) {
		/* Set windows directory. */
		homeDir.cd(WIN_PREFIX);
	}
#  endif /* defined(Q_OS_WIN) */

	if (homeDir.exists(confSubdir)) {
		/* Default configuration subdirectory in home is already present. */
		QString dir(homeDir.path() + QDir::separator() + confSubdir);
		if (!QFileInfo(dir).isWritable()) {
			logErrorNL(
			    "You don't have permissions to write into '%s'.",
			    dir.toUtf8().constData());
		}
		return dir;
	} else if (homeDir.exists() && QFileInfo(homeDir.path()).isWritable()) {
		/*
		 * Default configuration subdirectory in home directory is not
		 * present but home is writeable.
		 */
		return homeDir.path() + QDir::separator() + confSubdir;
	} else {
		/*
		 * There is no previous configuration and home directory cannot
		 * be written into. Is the application running inside a sandbox?
		 *
		 * On Windows:
		 * QStandardPaths::AppConfigLocation results in {HOME}/AppData/Local/{APPNAME},
		 * QStandardPaths::AppDataLocation results in {HOME}/AppData/Roaming/{APPNAME}.
		 * We need to use the Roaming directory on Windows.
		 *
		 * Now there is a problem with the application name set by
		 * QCoreApplication::setApplicationName() as this name is used
		 * in the path.
		 */
#  if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
		/*
		 * Still using confSubdir in order to let the user change
		 * the configuration via command line.
		 */
		return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) +
		    QDir::separator() + confSubdir;
#  else /* < Qt-5.4 */
#    warning "Compiling against version < Qt-5.4 which does not provide QStandardPaths::AppDataLocation."
		/*
		 * Falling back to default location which is inaccessible.
		 * If you want to evade this branch then use Qt-5.4 and newer.
		 */
		return homeDir.path() + QDir::separator() + confSubdir;
#  endif /* >= Qt-5.4 */
	}

#endif /* PORTABLE_APPLICATION */

#undef WIN_PREFIX
}

enum WriteFileState confFileFixBackSlashes(const QString &filePath)
{
	QString fileContent;
	QFile file(filePath);

	if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
		QTextStream in(&file);
		fileContent = in.readAll();
		file.close();
	} else {
		logErrorNL("Cannot open file '%s' for reading.",
		    filePath.toUtf8().constData());
		return WF_CANNOT_READ;
	}

	fileContent.replace(QString("\\"), QString("/"));

	if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
		QTextStream in(&file);
		file.reset();
		in << fileContent;
		file.close();
	} else {
		logErrorNL("Cannot write file '%s'.",
		    filePath.toUtf8().constData());
		return WF_CANNOT_WRITE_WHOLE;
	}

	return WF_SUCCESS;
}

enum WriteFileState confFileRemovePwdQuotes(const QString &filePath)
{
	QString fileContent;
	QFile file(filePath);

	if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
		QTextStream in(&file);
		fileContent = in.readAll();
		file.close();
	} else {
		logErrorNL("Cannot open file '%s' for reading.",
		    filePath.toUtf8().constData());
		return WF_CANNOT_READ;
	}

	fileContent.replace(QString("\""), QString(""));

	if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
		QTextStream in(&file);
		file.reset();
		in << fileContent;
		file.close();
	} else {
		logErrorNL("Cannot write file '%s'.",
		    filePath.toUtf8().constData());
		return WF_CANNOT_WRITE_WHOLE;
	}

	return WF_SUCCESS;
}

#define LOCALE_SRC_PATH "locale"

QString appLocalisationDir(void)
{
	QString dirPath;

#ifdef LOCALE_INST_DIR
	/*
	 * Search in installation location if supplied.
	 */

	dirPath = QString(LOCALE_INST_DIR);

	if (QFileInfo::exists(dirPath)) {
		return dirPath;
	} else {
		dirPath.clear();
	}
#endif /* LOCALE_INST_DIR */

	/* Search in application location. */
	dirPath = QCoreApplication::applicationDirPath();
#ifdef Q_OS_OSX
	{
		QDir directory(dirPath);
		if ("MacOS" == directory.dirName()) {
			directory.cdUp();
		}
		dirPath = directory.absolutePath() + QDir::separator() +
		    "Resources";
	}
#endif /* Q_OS_OSX */
	dirPath += QDir::separator() + QString(LOCALE_SRC_PATH);

	if (QFileInfo::exists(dirPath)) {
		return dirPath;
	} else {
		dirPath.clear();
	}

	return dirPath;
}

QString qtLocalisationDir(void)
{
	QString dirPath;

#ifdef LOCALE_INST_DIR
	/*
	 * This variable is set in UNIX-like systems. Use default Qt location
	 * directory.
	 */

#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
	dirPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
#else /* < Qt-6.0 */
	dirPath = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
#endif /* >= Qt-6.0 */

	if (QFileInfo::exists(dirPath)) {
		return dirPath;
	} else {
		dirPath.clear();
	}
#endif /* LOCALE_INST_DIR */

	/* Search in application location. */
	dirPath = QCoreApplication::applicationDirPath();
#ifdef Q_OS_OSX
	{
		QDir directory(dirPath);
		if ("MacOS" == directory.dirName()) {
			directory.cdUp();
		}
		dirPath = directory.absolutePath() + QDir::separator() +
		    "Resources";
	}
#endif /* Q_OS_OSX */
	dirPath += QDir::separator() + QString(LOCALE_SRC_PATH);

	if (QFileInfo::exists(dirPath)) {
		return dirPath;
	} else {
		dirPath.clear();
	}

	return dirPath;
}

/*!
 * @brief Return text file name.
 *
 * @param[in] textFile Text file identifier.
 * @return Pointer to C string or NULL on error.
 */
static inline
const char *textFileName(enum TextFile textFile)
{
#define LICENCE_FILE "COPYING"
	const char *fileName = NULL;

	switch (textFile) {
	case TEXT_FILE_LICENCE:
		fileName = LICENCE_FILE;
		break;
	default:
		break;
	}

	return fileName;
#undef LICENCE_FILE
}

/*!
 * @brief Expands to defined text files installation location.
 */
#define instLocationUnix() \
	QString(TEXT_FILES_INST_DIR)

/*!
 * @brief Adjust text file location for OS X/macOS package.
 *
 * @param[in] filePath Location as defined by executable.
 * @return Adjusted path.
 */
static inline
QString adjustForMacPackage(const QString &filePath)
{
	QDir directory(filePath);
	if (QLatin1String("MacOS") == directory.dirName()) {
		directory.cdUp();
	}
	return directory.absolutePath() + QDir::separator() +
	    QLatin1String("Resources");
}

QString expectedTextFilePath(enum TextFile textFile)
{
	QString filePath;

#if defined TEXT_FILES_INST_DIR
	filePath = instLocationUnix();
#elif defined Q_OS_OSX
	filePath = QCoreApplication::applicationDirPath();
	filePath = adjustForMacPackage(filePath);
#else /* !TEXT_FILES_INST_DIR && !Q_OS_OSX */
	filePath = QCoreApplication::applicationDirPath();
#endif /* TEXT_FILES_INST_DIR || Q_OS_OSX */

	return filePath + QDir::separator() + textFileName(textFile);
}

/*!
 * @brief Searches for the location of the supplied text file.
 *
 * @param[in] fName File name.
 * @return Full file path to sought file.
 */
static
QString suppliedTextFileLocation(const QString &fName)
{
	QString filePath;

#ifdef TEXT_FILES_INST_DIR
	/*
	 * Search in installation location if supplied.
	 */

	filePath = instLocationUnix() + QDir::separator() + fName;

	if (QFile::exists(filePath)) {
		return filePath;
	} else {
		filePath.clear();
	}
#endif /* TEXT_FILES_INST_DIR */

	/* Search in application location. */
	filePath = QCoreApplication::applicationDirPath();
#ifdef Q_OS_OSX
	filePath = adjustForMacPackage(filePath);
#endif /* Q_OS_OSX */
	filePath += QDir::separator() + fName;

	if (QFile::exists(filePath)) {
		return filePath;
	} else {
		filePath.clear();
	}

	return filePath;
}

QString suppliedTextFileContent(enum TextFile textFile)
{
	const char *fileName = textFileName(textFile);
	if (Q_UNLIKELY(NULL == fileName)) {
		Q_ASSERT(0);
		return QString();
	}

	QString fileLocation = suppliedTextFileLocation(fileName);
	if (fileLocation.isEmpty()) {
		return QString();
	}

	QString content;

	QFile file(fileLocation);
	QTextStream textStream(&file);
	if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
		content = textStream.readAll();
	}

	file.close();

	return content;
}

#undef LOCALE_SRC_PATH
