/*
 * Copyright (C) 2014-2021 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 <cinttypes>
#include <QCoreApplication>
#include <QDir>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QProcess>
#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
#  include <QStorageInfo>
#else /* < Qt-5.4 */
#  warning "Compiling against version < Qt-5.4 which does not have QStorageInfo."
#endif /* >= Qt-5.4 */
#include <QString>

#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/utility/strings.h"
#include "src/gui/dlg_update_portable.h"
#include "ui_dlg_update_portable.h"

CopyDirThread::CopyDirThread(const QString &srcDir, const QString &tgtDir,
    QObject *parent)
    : QThread(parent),
    m_success(false),
    m_srcDir(srcDir),
    m_tgtDir(tgtDir)
{
}

/*!
 * @brief Recursive directory copying.
 *
 * @param[in] srcDir Source directory.
 * @param[in] tgtDir Target directory.
 * @return True on success.
 */
static
bool recursiveCopy(const QString &srcDir, const QString &tgtDir)
{
	bool success = false;

	QDir sDir(srcDir);
	if (!sDir.exists()) {
		return false;
	}

	QDir dDir(tgtDir);
	if (!dDir.exists()) {
		dDir.mkdir(tgtDir);
	}

	QStringList files =
	    sDir.entryList(QDir::Files | QDir::System | QDir::Hidden);
	foreach (const QString &file, files) {
		QString srcName = srcDir + QDir::separator() + file;
		QString destName = tgtDir + QDir::separator() + file;
		success = QFile::copy(srcName, destName);
		if (!success) {
			return false;
		}
	}

	files.clear();
	files = sDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
	foreach (const QString &file, files) {
		QString srcName = srcDir + QDir::separator() + file;
		QString destName = tgtDir + QDir::separator() + file;
		success = recursiveCopy(srcName, destName);
		if (!success) {
			return false;
		}
	}

	return true;
}

void CopyDirThread::run(void)
{
	m_success = recursiveCopy(m_srcDir, m_tgtDir);
}

class UpdatePortableData {
public:
	UpdatePortableData(const QString &path, const QString &ver,
	    const QString &confPath)
	    : pkgPath(path), pkgVer(ver), workConfPath(confPath)
	{ }

	QString pkgPath; /*!< Package path. */
	QString pkgVer; /*!< Package version. */
	QString workConfPath; /*!< Current working configuration directory. */
};

UpdatePortableData *DlgUpdatePortable::updatePortableData = Q_NULLPTR;

DlgUpdatePortable::DlgUpdatePortable(QWidget *parent)
    : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgUpdatePortable)
{
	m_ui->setupUi(this);

	initDialogue();
}

DlgUpdatePortable::~DlgUpdatePortable(void)
{
	delete m_ui;
}

bool DlgUpdatePortable::updatePortable(QWidget *parent)
{
	DlgUpdatePortable dlg(parent);
	dlg.exec();
	return true;
}

UpdatePortableData *DlgUpdatePortable::createUpdatePortableData(
    const QString &pkgPath, const QString &pkgVer, const QString &workConfPath)
{
	if (Q_UNLIKELY(pkgPath.isEmpty() || pkgVer.isEmpty() ||
	        workConfPath.isEmpty())) {
		return Q_NULLPTR;
	}

	return new (::std::nothrow) UpdatePortableData(pkgPath, pkgVer,
	    workConfPath);
}

void DlgUpdatePortable::changeDirectory(void)
{
	QString newDir = QFileDialog::getExistingDirectory(this,
	    tr("Select Directory"), m_ui->pathEdit->text(),
	    QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
	if (!newDir.isEmpty()) {
		m_ui->pathEdit->setText(QDir::toNativeSeparators(newDir));
	}
}

/*!
 * @brief Recursively compute total directory size in bytes.
 *
 * @param[in] dirPath Directory path.
 * @return Size in bytes.
 */
static
qint64 dirSizeBytes(const QString &dirPath)
{
	qint64 size = 0;

	const QDir dir(dirPath);
	if (Q_UNLIKELY(!dir.exists())) {
		return size;
	}

	/* Calculate total size of current directories */
	QDir::Filters fileFilters = QDir::Files | QDir::System | QDir::Hidden;
	foreach (const QString &filePath, dir.entryList(fileFilters)) {
		QFileInfo fi(dir, filePath);
		size += fi.size();
	}
	/* Add size of child directories recursively */
	QDir::Filters dirFilters = QDir::Dirs | QDir::NoDotAndDotDot
	    | QDir::System | QDir::Hidden;
	foreach (const QString &childDir, dir.entryList(dirFilters)) {
		size += dirSizeBytes(dirPath + QDir::separator() + childDir);
	}

	return size;
}

/*!
 * @brief Compute the free space needed for the package extraction.
 *
 * @param[in] confDirPath Configuration directory.
 * @teturn Required space in bytes.
 */
static
qint64 neededFreeSpace(const QString &confDirPath)
{
	qint64 neededSpace = 100000000; /* Roughly 100 MB for the application. */

	if (!confDirPath.isEmpty()) {
		qint64 dirSize = dirSizeBytes(confDirPath);
		logInfoNL("Directory '%s' occupies %" PRId64 " bytes.",
		    confDirPath.toUtf8().constData(), UGLY_QINT64_CAST dirSize);
		neededSpace += dirSize;
	}

	return neededSpace;
}

void DlgUpdatePortable::processUserInput(void)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
	const QStorageInfo storageInfo(m_ui->pathEdit->text());

	const QString fsType = storageInfo.fileSystemType();
	logInfoNL("Detected '%s' file system.", fsType.toUtf8().constData());
	if (fsType.toLower().contains("fat32")) {
		m_ui->fsTypeNotificationLabel->setStyleSheet("QLabel { color : red; }");
		m_ui->fsTypeNotificationLabel->setText(tr(
		    "The selected directory is on a %1 file system. "
		    "This file system may not be able to store very large files. "
		    "Consider using NTFS or exFAT to avoid these types of problems.").arg(fsType));
	} else {
		m_ui->fsTypeNotificationLabel->setText(QString());
	}

	QString confDirPath;
	if (m_ui->storageCopyButton->isChecked()) {
		if (updatePortableData != Q_NULLPTR) {
			confDirPath = updatePortableData->workConfPath;
		} else {
			logErrorNL("%s", "Cannot access portable update data.");
		}
	}
	qint64 spaceNeeded = neededFreeSpace(confDirPath);
	if (storageInfo.bytesAvailable() < spaceNeeded) {
		m_ui->freeSpaceNotificationLabel->setStyleSheet("QLabel { color : red; }");
		m_ui->freeSpaceNotificationLabel->setText(tr(
		    "There is not enough space on device '%1'. "
		    "At least %2 is required.")
		        .arg(storageInfo.device().constData())
		        .arg(Utility::sizeString(spaceNeeded)));
		m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
	} else {
		m_ui->freeSpaceNotificationLabel->setText(QString());
		m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
	}
#else /* < Qt-5.4 */
	m_ui->fsTypeNotificationLabel->setText(QString());
	m_ui->freeSpaceNotificationLabel->setText(QString());
	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
#endif /* >= Qt-5.4 */
}

void DlgUpdatePortable::extractPackage(void)
{
	if (Q_UNLIKELY(updatePortableData == Q_NULLPTR)) {
		logErrorNL("%s", "Cannot access portable update data.");
		return;
	}

	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);

	m_ui->pathEdit->setEnabled(false);
	m_ui->changePathButton->setEnabled(false);
	m_ui->storageDontMoveButton->setEnabled(false);
	m_ui->storageCopyButton->setEnabled(false);

	m_ui->progress->setValue(0);
	m_ui->progress->setMinimum(0);
	m_ui->progress->setMaximum(0);
	m_ui->progress->setVisible(true);

	m_ui->progressLabel->setText(tr("Extracting package..."));
	m_ui->progressLabel->setVisible(true);
	{
		const QStringList arguments = QStringList()
		    << "-NoP"
		    << "-NonI"
		    << "-Command"
		    << "Expand-Archive"
		    << "'" + QDir::fromNativeSeparators(updatePortableData->pkgPath) + "'"
		    << "'" + QDir::fromNativeSeparators(m_ui->pathEdit->text()) + "'";

		/* Run PowerShell command. */
		QProcess *process = new (::std::nothrow) QProcess(this);
		if (Q_UNLIKELY(process == Q_NULLPTR)) {
			logErrorNL("%s", "Cannot create extraction process.");
			close();
			return;
		}
		connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
		    this, SLOT(extractionProcessFinished(int, QProcess::ExitStatus)));

		process->start("powershell.exe", arguments);

		if (Q_UNLIKELY(!process->waitForStarted(2000))) {
			logErrorNL(
			    "Package extraction process failed to start with error code '%d'.",
			    process->error());
			QMessageBox::warning(this, tr("Error extracting package"),
			    tr("Could not extract the package '%1' into destination directory '%2'.")
			        .arg(updatePortableData->pkgPath)
			        .arg(m_ui->pathEdit->text()));
			delete process; process = Q_NULLPTR;
			close();
		}
	}
}

void DlgUpdatePortable::extractionProcessFinished(int exitCode,
    QProcess::ExitStatus exitStatus)
{
	Q_UNUSED(exitCode);

	if (Q_UNLIKELY(updatePortableData == Q_NULLPTR)) {
		Q_ASSERT(0);
		close();
		return;
	}

	m_ui->progress->setValue(0);
	m_ui->progress->setMinimum(0);
	m_ui->progress->setMaximum(0);
	m_ui->progress->setVisible(false);

	m_ui->progressLabel->setText(QString());
	m_ui->progressLabel->setVisible(false);

	{
		QProcess *process = qobject_cast<QProcess *>(QObject::sender());
		if (process != Q_NULLPTR) {
			process->deleteLater();
		} else {
			logErrorNL("%s", "Cannot obtain sender process.");
		}
	}

	if (Q_UNLIKELY(exitStatus == QProcess::CrashExit)) {
		logErrorNL("%s", "Package extraction failed.");
		QMessageBox::warning(this, tr("Error extracting package"),
		    tr("Could not extract the package '%1' into destination directory '%2'.")
		        .arg(updatePortableData->pkgPath)
		        .arg(m_ui->pathEdit->text()));
		close();
		return;
	}

	if (!m_ui->storageCopyButton->isChecked()) {
		QMessageBox::information(this, tr("Package extracted"),
		    tr("Package has been extracted into '%1'.")
		        .arg(m_ui->pathEdit->text()));
	} else {
		{
			QDir tgtPkgDir(m_ui->pathEdit->text());
			if (Q_UNLIKELY(!tgtPkgDir.exists())) {
				QMessageBox::warning(this, tr("Error copying data"),
				    tr("Could not find target directory '%1'.")
				        .arg(m_ui->pathEdit->text()));
				close();
				return;
			}
		}

		m_ui->progress->setValue(0);
		m_ui->progress->setMinimum(0);
		m_ui->progress->setMaximum(0);
		m_ui->progress->setVisible(true);

		m_ui->progressLabel->setText(tr("Copying data..."));
		m_ui->progressLabel->setVisible(true);

		const QString confDirPath = updatePortableData->workConfPath;
		/* Directory root name inside the zip package. */
		const QString zipRootDir(QString("datovka-%1-portable").arg(updatePortableData->pkgVer));

		const QString confDirName = QDir(confDirPath).dirName();

		const QString target(QDir::toNativeSeparators(m_ui->pathEdit->text() +
		    QDir::separator() + zipRootDir + QDir::separator() + confDirName));

		CopyDirThread *thread = new (::std::nothrow) CopyDirThread(confDirPath, target, this);
		if (Q_UNLIKELY(thread == Q_NULLPTR)) {
			logErrorNL("%s", "Cannot create copy thread.");
			close();
			return;
		}
		connect(thread, SIGNAL(finished()),
		    this, SLOT(copyThreadFinished()));

		thread->start();
	}
}

void DlgUpdatePortable::copyThreadFinished(void)
{
	m_ui->progress->setValue(0);
	m_ui->progress->setMinimum(0);
	m_ui->progress->setMaximum(0);
	m_ui->progress->setVisible(false);

	m_ui->progressLabel->setText(QString());
	m_ui->progressLabel->setVisible(false);

	CopyDirThread *thread = qobject_cast<CopyDirThread *>(QObject::sender());
	if (Q_UNLIKELY(thread == Q_NULLPTR)) {
		logErrorNL("%s", "Cannot obtain sender thread.");
		close();
		return;
	}

	if (thread->m_success) {
		QMessageBox::information(this, tr("Package extracted and data copied"),
		    tr("Package has been extracted into '%1'. "
		       "Stored data have been copied into '%2'.")
		        .arg(m_ui->pathEdit->text()).arg(thread->m_tgtDir));
	} else {
		QMessageBox::warning(this, tr("Cannot copy data"),
		    tr("Could not copy data from '%1' to '%2'.")
		        .arg(thread->m_srcDir).arg(thread->m_tgtDir));
	}

	thread->deleteLater();

	close();
}

void DlgUpdatePortable::initDialogue(void)
{
	m_ui->descriptionLabel->setText(
	    tr("Select the location where to extract the new package and specify what to do with the data stored in this version."));

	m_ui->pathEdit->setReadOnly(true);
	{
		/* Get Parent directory of current working directory. */
		QFileInfo fileInfo(QCoreApplication::applicationDirPath());
		QString parentDir = fileInfo.dir().absolutePath();
		m_ui->pathEdit->setText(QDir::toNativeSeparators(parentDir));
	}
	connect(m_ui->pathEdit, SIGNAL(textChanged(QString)),
	    this, SLOT(processUserInput()));

	connect(m_ui->changePathButton, SIGNAL(clicked()),
	    this, SLOT(changeDirectory()));

	m_ui->storageDontMoveButton->setChecked(true);
	m_ui->storageCopyButton->setChecked(false);
	connect(m_ui->storageDontMoveButton, SIGNAL(toggled(bool)),
	    this, SLOT(processUserInput()));
	connect(m_ui->storageCopyButton, SIGNAL(toggled(bool)),
	    this, SLOT(processUserInput()));

	m_ui->fsTypeNotificationLabel->setWordWrap(true);
	m_ui->fsTypeNotificationLabel->setText(QString());
	m_ui->freeSpaceNotificationLabel->setWordWrap(true);
	m_ui->freeSpaceNotificationLabel->setText(QString());

	processUserInput();

	m_ui->progress->setValue(0);
	m_ui->progress->setMinimum(0);
	m_ui->progress->setMaximum(100);
	m_ui->progress->setVisible(false);

	m_ui->progressLabel->setText(QString());
	m_ui->progressLabel->setVisible(false);

	m_ui->buttonBox->setStandardButtons(
	    QDialogButtonBox::Ok | QDialogButtonBox::Cancel);

	connect(m_ui->buttonBox, SIGNAL(accepted()),
	    this, SLOT(extractPackage()));
	connect(m_ui->buttonBox, SIGNAL(rejected()),
	    this, SLOT(reject()));
}
