/*
 * Copyright (C) 2014-2026 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 <QDir>
#if (QT_VERSION < QT_VERSION_CHECK(5, 9, 0))
#  include <QDirIterator>
#endif /* < Qt-5.9 */
#include <QFileDialog>
#include <QFile>
#include <QFileInfo>
#include <QMessageBox>
#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 <QStringBuilder>
#include <QtGlobal> /* Q_INT64_C */
#include <quazip/quazip.h> /* bundled or system */
#include <quazip/quazipdir.h> /* bundled or system */
#include <quazip/quazipfile.h> /* bundled or system */

#include "src/datovka_shared/identifiers/account_id.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/utility/strings.h"
#include "src/datovka_shared/worker/pool.h"
#include "src/dimensions/dimensions.h"
#include "src/global.h"
#include "src/gui/dlg_convert_export_for_mobile_app.h"
#include "src/gui/helper.h"
#include "src/io/account_db.h"
#include "src/io/message_db_set.h"
#include "src/json/backup.h"
#include "src/settings/accounts.h"
#include "src/settings/prefs_specific.h"
#include "src/views/table_home_end_filter.h"
#include "src/views/table_space_selection_filter.h"
#include "src/views/table_tab_ignore_filter.h"
#include "ui_dlg_convert_export_for_mobile_app.h"

#define SPACE_UNKNOWN -1

#define macroAccountName(aId) \
	GlobInstcs::acntMapPtr->acntData(aId).accountName()

#define macroDataBoxId(uname) \
	(GlobInstcs::accntDbPtr->dbId(AccountDb::keyFromLogin(uname)))

static const QString dlgName("convert_export_for_mobile_app");
static const QString accountTableName("account_list");

/*!
 * @brief Add data into the account model.
 *
 * @param[in,out] model Account model.
 * @param[in] acntIdDbList Account list.
 */
static
void initialiseAccountModel(BackupSelectionModel &model,
    const QList<AcntIdDb> &acntIdDbList)
{
	for (const AcntIdDb &acntIdDb : acntIdDbList) {
		const AcntId acntId(acntIdDb);
		model.appendData(false, macroAccountName(acntId), acntId,
		    macroDataBoxId(acntIdDb.username()));
	}
}

/*!
 * @brief Converts the directory path
 *
 * @note The SQLite engine expects '/' to be used as directory separator,
 *     even on Windows.
 *
 * @param[in] nativeDir Directory path with native separators.
 * @param[in] parent Parent widget.
 * @return Path to existing directory, null string on any error.
 */
static
QString normalisedDir(const QString &nativeDir, QWidget *parent = Q_NULLPTR)
{
	if (Q_UNLIKELY(nativeDir.isEmpty())) {
		Q_ASSERT(0);
		return QString();
	}

	const QString normDir(QDir::fromNativeSeparators(nativeDir));
	{
		QFileInfo fInfo(normDir);
		if (Q_UNLIKELY(fInfo.exists() && !fInfo.isDir())) {
			QMessageBox::warning(parent,
			    DlgConvertExportForMobileApp::tr("Not a Directory"),
			    DlgConvertExportForMobileApp::tr("The path '%1' is not a directory.")
			        .arg(nativeDir),
			    QMessageBox::Ok, QMessageBox::Ok);
			return QString();
		}
	}
	QDir dir(normDir);
	if (!dir.exists()) {
		QMessageBox::StandardButton reply = QMessageBox::question(parent,
		    DlgConvertExportForMobileApp::tr("Non-Existent Directory"),
		    DlgConvertExportForMobileApp::tr(
		        "The specified directory does not exist. Do you want to create the directory '%1'?")
		            .arg(nativeDir),
		    QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes);
		if (Q_UNLIKELY(reply == QMessageBox::No)) {
			return QString();
		}
		if (Q_UNLIKELY(!dir.mkpath("."))) {
			QMessageBox::warning(parent,
			    DlgConvertExportForMobileApp::tr("Cannot Create Directory"),
			    DlgConvertExportForMobileApp::tr("Could not create the directory '%1'.")
			        .arg(nativeDir),
			    QMessageBox::Ok, QMessageBox::Ok);
			return QString();
		}
	}

	QFileInfo fInfo(normDir);
	if (!fInfo.isDir() || !fInfo.isWritable()) {
		QMessageBox::warning(parent,
		    DlgConvertExportForMobileApp::tr("Cannot Write into Directory"),
		    DlgConvertExportForMobileApp::tr(
		        "You don't have permissions to write into directory '%1'.")
		            .arg(nativeDir),
		    QMessageBox::Ok, QMessageBox::Ok);
		return QString();
	}

	return normDir;
}

/*!
 * @brief Implements QDir::isEmpty() for Qt < Qt-5.9.
 *
 * @param[in] dir Directory.
 * @param[in] filters Filters.
 * @return Count == 0 with default filters.
 */
static inline
bool isEmptyDir(const QDir &dir,
    QDir::Filters filters = QDir::Filters(QDir::AllEntries | QDir::NoDotAndDotDot))
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0))
	return dir.isEmpty(filters);
#else /* < Qt-5.9 */
	/* Equivalent to \c{count() == 0} with filters. */
	const QDirIterator it(dir.path(), dir.nameFilters(), filters);
	return !it.hasNext();
#endif /* >= Qt-5.9 */
}

/*!
 * @brief Join two directories to path.
 * @param[in] dirName1 First directory name.
 * @param[in] dirName2 Second directory name.
 * @return Directory path.
 */
static
QString joinDirs(const QString &dirName1, const QString &dirName2)
{
	return dirName1 % QDir::separator() % dirName2;
}

/*!
 * @brief Construct message zfo file name.
 *
 * @param[in] dmId Message ID.
 * @param[in] msgType Message type.
 * @return Return message zfo file name.
 */
static
QString getMsgZfoName(qint64 dmId, enum MessageDb::MessageType msgType)
{
	switch (msgType) {
	case MessageDb::TYPE_RECEIVED:
		return QString("DDZ_%1.zfo").arg(dmId);
		break;
	case MessageDb::TYPE_SENT:
		return QString("ODZ_%1.zfo").arg(dmId);
		break;
	default:
		return QString("DZ_%1.zfo").arg(dmId);
		break;
	}
}

/*!
 * @brief Construct delivery info zfo file name.
 *
 * @param[in] dmId Message ID.
 * @return Return delivery info zfo file name.
 */
static
QString getDelInfoZfoName(qint64 dmId)
{
	return QString("DD_%1.zfo").arg(dmId);
}

/*!
 * @brief Construct account directory name where to store the ZFO's. It doesn't
 *    contain the full path.
 *
 * @param[in] acntId Account id.
 * @return Account zfo directory name string.
 */
static
QString getAccountZfoDir(const AcntId &acntId)
{
	return QString("%1" DB_UNDERSCORES "%2")
	    .arg(acntId.username()).arg(acntId.testing() ? DB_MARKER_TEST : DB_MARKER_PROD);
}

/*!
 * @brief Construct full path to zfo file.
 *
 * @param[in] acntId Account id.
 * @param[in] dmId Message ID.
 * @param[in] dstDir Target directory where data will be stored.
 * @return Full path to zfo file.
 */
static
QString getZfoPath(const AcntId &acntId, qint64 dmId, const QString &dstDir)
{
	return joinDirs(dstDir, joinDirs(getAccountZfoDir(acntId),
	    QString::number(dmId)));
}

/*!
 * @brief Create full path to zfo file.
 *
 * @param[in] acntId Account id.
 * @param[in] dmId Message ID.
 * @param[in] dstDir Target directory where data will be stored.
 * @return Full path to zfo file.
 */
static
QString createZfoTargetDir(const AcntId &acntId, qint64 dmId,
    const QString &dstDir)
{
	QString newPath(getZfoPath(acntId, dmId, dstDir));

	QDir dir(newPath);
	if (Q_UNLIKELY(!dir.exists() && !dir.mkpath("."))) {
		logErrorNL("Location '%s' could not be created.",
		    newPath.toUtf8().constData());
		return QString();
	}
	Q_ASSERT(dir.exists());

	return newPath;
}

/*!
 * @brief Wrtie zfo file.
 *
 * @param[in] acntId Account id.
 * @param[in] dmId Message id.
 * @param[in] zfoFileName Message zfo name.
 * @param[in] zfoData Message zfo data.
 * @param[in] dstDir Target directory where data will be stored.
 * @return True on success.
 */
static
bool writeZfoToLocalStorage(const AcntId &acntId, qint64 dmId,
    const QString &zfoFileName, const QByteArray &zfoData, const QString &dstDir)
{
	QString zfoTargetPath = createZfoTargetDir(acntId, dmId, dstDir);

	if (Q_UNLIKELY(zfoTargetPath.isEmpty())) {
		logErrorNL("Cannot create ZFO target path for file '%s'.",
		    zfoFileName.toUtf8().constData());
		return false;
	}

	QFile fout(joinDirs(zfoTargetPath, zfoFileName));
	if (Q_UNLIKELY(!fout.open(QIODevice::WriteOnly))) {
		logErrorNL("Cannot open file '%s'.",
		    zfoFileName.toUtf8().constData());
		return false;
	}

	const QString fullName = fout.fileName();
	qint64 written = fout.write(zfoData);
	bool flushed = fout.flush();
	fout.close();

	if (Q_UNLIKELY((written != zfoData.size()) || !flushed)) {
		QFile::remove(fullName);
		return false;
	}

	return true;
}

/*!
 * @brief Export zfo delivery info.
 *
 * @param[in] msgDb Message database.
 * @param[in] acntId Account id.
 * @param[in] dstDir Target directory where data will be stored.
 * @return True on success.
 */
static
bool exportDelInfo(MessageDb *msgDb, const AcntId &acntId,
    const QString &dstDir)
{
	bool ret = true;
	QList<qint64> ids = msgDb->getAllMessageIDs(MessageDb::TYPE_RECEIVED);
	ids.append(msgDb->getAllMessageIDs(MessageDb::TYPE_SENT));

	for (qint64 msgId : ids) {
		QByteArray data = msgDb->getDeliveryInfoRaw(msgId);
		if (data.isEmpty()) {
			continue;
		}
		ret = ret && writeZfoToLocalStorage(acntId, msgId,
		    getDelInfoZfoName(msgId), data, dstDir);
	}

	return ret;
}

/*!
 * @brief Export zfo messages.
 *
 * @param[in] msgDb Message database.
 * @param[in] acntId Account id.
 * @param[in] msgType Message type.
 * @param[in] dstDir Target directory where data will be stored.
 * @return True on success.
 */
static
bool exportMsg(MessageDb *msgDb, const AcntId &acntId,
    enum MessageDb::MessageType msgType, const QString &dstDir)
{
	bool ret = true;
	QList<qint64> ids = msgDb->getAllMessageIDs(msgType);

	for (qint64 msgId : ids) {
		QByteArray data = msgDb->getCompleteMessageRaw(msgId);
		if (data.isEmpty()) {
			continue;
		}
		ret = ret && writeZfoToLocalStorage(acntId, msgId,
		    getMsgZfoName(msgId, msgType), data, dstDir);
	}

	return ret;
}

/*!
 * @brief Get relative file name path in ZIP archive.
 *
 * @param[in] fileInfo File info of source file.
 * @param[in] sourceDirPath Source directory path where data are stored.
 * @param[in] subDir Target sub directory where data will be stored.
 * @return Relative file name path in ZIP archive.
 */
static
QString fileNameWithRelativePath(const QFileInfo &fileInfo,
    const QDir &sourceDirPath, const QString &subDir)
{
	QString fileNameWithRelativePath = fileInfo.filePath().remove(
	    0, sourceDirPath.absolutePath().length() + 1);
	if (!subDir.isEmpty()) {
		fileNameWithRelativePath = joinDirs(subDir,
		    fileNameWithRelativePath);
	}
	return fileNameWithRelativePath;
}

/*!
 * @brief Get list of all files in source directory tree.
 *
 * @param[in] dir Source directory path.
 * @return List of all files in directory tree.
 */
static
QStringList recurseAddDir(const QDir &dir) {

	QStringList fileList;
	QStringList eList = dir.entryList(QDir::NoDotAndDotDot |
	    QDir::Dirs | QDir::Files);

	for (const QString &file : eList) {
		QFileInfo fi(joinDirs(dir.path(),file));
		if (fi.isSymLink()) {
			return QStringList();
		}
		if (fi.isDir()) {
			QDir sd(fi.filePath());
			fileList.append(recurseAddDir(sd));
		} else {
			fileList << QDir::toNativeSeparators(fi.filePath());
		}
	}
	return fileList;
}

/*!
 * @brief Create subdirectory in the path.
 *
 * @param[in] sourcePath Source path.
 * @param[in] subDir Subdirectory name.
 * @return Absolute subdirectory path.
 */
static
QString createSubDirPath(const QString &sourcePath, const QString &subDir)
{
	QDir dir(sourcePath % QDir::separator() % subDir);
	dir.removeRecursively();
	if (Q_UNLIKELY((!dir.exists()) && (!dir.mkpath(".")))) {
		return QString();
	}
	return dir.absolutePath();
}

/*!
 * @brief Remove directory recursively.
 *
 * @param[in] dirPath Directory path.
 */
static inline
void removeRecursivelyDir(const QString &dirPath)
{
	QDir dir(dirPath);
	dir.removeRecursively();
}

/*!
 * @brief Compute file checksum.
 *
 * @param[in] filePath File path.
 * @param[in] algorithm Algorithm to use for the checksum.
 * @return Checksum, null checksum on failure.
 */
static
Json::Backup::Checksum computeFileChecksum(const QString &filePath,
    enum Json::Backup::Checksum::Algorithm
    algorith = Json::Backup::Checksum::ALG_SHA512)
{
	QFile f(filePath);
	if (f.open(QFile::ReadOnly)) {
		return Json::Backup::Checksum::computeChecksum(&f, algorith);
	}
	return Json::Backup::Checksum();
}

/*!
 * @brief Add file into ZIP archive.
 *
 * @param[in] fileInfo File info of source file.
 * @param[in] sourceDirPath Source directory path where data are stored.
 * @param[in] subDir Target sub directory where data will be stored.
 * @param[in,out] quaZipFile Qua zip file stored into ZIP archive.
 * @return True if success.
 */
static
bool addFileIntoZip(const QFileInfo &fileInfo, const QDir &sourceDirPath,
    const QString &subDir, QuaZipFile &quaZipFile)
{
	QFile inFile(fileInfo.filePath());
	if (Q_UNLIKELY(!inFile.open(QIODevice::ReadOnly))) {
		return false;
	}
	if (Q_UNLIKELY(!quaZipFile.open(QIODevice::WriteOnly,
	        QuaZipNewInfo(fileNameWithRelativePath(fileInfo,
	        sourceDirPath, subDir), fileInfo.filePath())))) {
		goto fail;
	}
	if (Q_UNLIKELY(quaZipFile.write(inFile.readAll()) != inFile.size())) {
		quaZipFile.close();
		goto fail;
	}
	quaZipFile.close();
	if (Q_UNLIKELY(quaZipFile.getZipError() != UNZ_OK)) {
		goto fail;
	}
	inFile.close();
	return true;
fail:
	inFile.close();
	return false;
}

/*!
 * @brief Create JSON transfer content.
 *
 * @param[in] files List of files and their checksums.
 * @return JSON content.
 */
static
QByteArray createTransferJson(const QList<Json::Backup::File> &files)
{
	Json::Backup bu;
	bu.setType(Json::Backup::BUT_DESKTOP_TO_MOBILE);
	bu.setDateTime(QDateTime::currentDateTime());
	bu.setAppInfo(Json::Backup::AppInfo(
	    QString(APP_NAME), QString("mobile"), QString(VERSION)));
	bu.setFiles(files);
	return bu.toJsonData(true);
}

/*!
 * @brief Add JSON file into ZIP archive.
 *
 * @param[in] jsonName File name.
 * @param[in] jsonData File data.
 * @param[in,out] quaZipFile Qua zip file stored into ZIP archive.
 * @return True if success.
 */
static
bool addJsonIntoZip(const QString &jsonName, const QByteArray &jsonData,
    QuaZipFile &quaZipFile)
{
	if (Q_UNLIKELY(!quaZipFile.open(QIODevice::WriteOnly,
	        QuaZipNewInfo(jsonName)))) {
		return false;
	}
	if (Q_UNLIKELY(quaZipFile.write(jsonData) != jsonData.size())) {
		quaZipFile.close();
		return false;
	}
	quaZipFile.close();
	if (Q_UNLIKELY(quaZipFile.getZipError() != UNZ_OK)) {
		return false;
	}
	return true;
}

DlgConvertExportForMobileApp::DlgConvertExportForMobileApp(const QList<AcntIdDb> &acntIdDbList,
    QWidget *parent) : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgConvertExportForMobileApp),
    m_acntIdDbList(acntIdDbList),
    m_backupVolume(),
    m_availableSpace(SPACE_UNKNOWN),
    m_requiredSpace(SPACE_UNKNOWN),
    m_backupSelectionModel(this)
{
	m_ui->setupUi(this);
	initDialogue();
	initialiseAccountModel(m_backupSelectionModel, m_acntIdDbList);
	computeRequiredSpace();
	enableOkButton();
}

DlgConvertExportForMobileApp::~DlgConvertExportForMobileApp(void)
{
	PrefsSpecific::setDlgTblRelColWidths(*GlobInstcs::prefsPtr,
	    dlgName, accountTableName,
	    Dimensions::relativeTableColumnWidths(m_ui->accountView));
	PrefsSpecific::setDlgTblColSortOrder(*GlobInstcs::prefsPtr,
	    dlgName, accountTableName,
	    Dimensions::tableColumnSortOrder(m_ui->accountView));
	delete m_ui;
}

void DlgConvertExportForMobileApp::transfer(const QList<AcntIdDb> &acntIdDbList,
    QWidget *parent)
{
	if (Q_UNLIKELY(GlobInstcs::workPoolPtr->working())) {
		logErrorNL("%s", "Cannot back up application content with a running worker pool.");
		Q_ASSERT(0);
		return;
	}

	DlgConvertExportForMobileApp dlg(acntIdDbList, parent);

	const QSize dfltSize = dlg.size();
	{
		const QSize newSize = Dimensions::dialogueSize(&dlg,
		    PrefsSpecific::dlgSize(*GlobInstcs::prefsPtr, dlgName),
		    dfltSize);
		if (newSize.isValid()) {
			dlg.resize(newSize);
		}
	}

	dlg.exec();

	PrefsSpecific::setDlgSize(*GlobInstcs::prefsPtr, dlgName,
	    dlg.size(), dfltSize);
}

void DlgConvertExportForMobileApp::showEvent(QShowEvent *event)
{
	QDialog::showEvent(event);

	Dimensions::setRelativeTableColumnWidths(m_ui->accountView,
	    PrefsSpecific::dlgTblRelColWidths(*GlobInstcs::prefsPtr,
	    dlgName, accountTableName));
	Dimensions::setTableColumnSortOrder(m_ui->accountView,
	    PrefsSpecific::dlgTblColSortOrder(*GlobInstcs::prefsPtr,
	    dlgName, accountTableName));
}

void DlgConvertExportForMobileApp::computeAvailableSpace(const QString &dirPath)
{
	if (dirPath.isEmpty()) {
		m_ui->availableSpaceLabel->setText(QString());
		return;
	}

#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
	{
		const QStorageInfo info(dirPath);
		m_backupVolume = info.displayName();
		m_availableSpace = info.bytesAvailable();
		if (Q_UNLIKELY(m_availableSpace < 0)) {
			m_availableSpace = SPACE_UNKNOWN;
		}
	}
#else /* < Qt-5.4 */
	m_backupVolume.clear();
	m_availableSpace = SPACE_UNKNOWN;
#endif /* >= Qt-5.4 */
	QString volume;
	if (!m_backupVolume.isEmpty()) {
		volume = QStringLiteral(" '") % m_backupVolume % QStringLiteral("' ");
	}
	m_ui->availableSpaceLabel->setText(tr("Available space on volume %1: %2").arg(volume).arg(Utility::sizeString(m_availableSpace)));

	showSizeNotificationIfNeeded();
}

void DlgConvertExportForMobileApp::chooseDirectory(void)
{
	QString dirPath(QFileDialog::getExistingDirectory(this,
	    tr("Select Directory"), QString(),
	    QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks));
	if (!dirPath.isEmpty()) {
		m_ui->directoryLine->setText(QDir::toNativeSeparators(dirPath));
	}
}

void DlgConvertExportForMobileApp::applyAllSelection(int state)
{
	/* Temporarily disconnect actions which may trigger this slot. */
	m_backupSelectionModel.disconnect(
	    SIGNAL(dataChanged(QModelIndex, QModelIndex)),
	    this, SLOT(modifiedSelection()));

	switch (state) {
	case Qt::Unchecked:
		m_ui->allCheckBox->setTristate(false);
		m_backupSelectionModel.setAllCheck(false);
		break;
	case Qt::PartiallyChecked:
		/* Do nothing. */
		break;
	case Qt::Checked:
		m_ui->allCheckBox->setTristate(false);
		m_backupSelectionModel.setAllCheck(true);
		break;
	default:
		Q_ASSERT(0);
		break;
	}

	/* Reconnect actions. */
	connect(&m_backupSelectionModel,
	    SIGNAL(dataChanged(QModelIndex, QModelIndex)),
	    this, SLOT(modifiedSelection()));

	computeRequiredSpace();
}

void DlgConvertExportForMobileApp::modifiedSelection(void)
{
	enum Qt::CheckState state = Qt::PartiallyChecked;
	if (((m_backupSelectionModel.rowCount() == 0) ||
	     (m_backupSelectionModel.numChecked() == m_backupSelectionModel.rowCount()))) {
		state = Qt::Checked;
	} else if ((m_backupSelectionModel.numChecked() == 0)) {
		state = Qt::Unchecked;
	}

	m_ui->allCheckBox->setTristate(state == Qt::PartiallyChecked);
	m_ui->allCheckBox->setCheckState(state);

	computeRequiredSpace();
}

void DlgConvertExportForMobileApp::enableOkButton(void)
{
	bool dirOk = !m_ui->directoryLine->text().isEmpty();
	bool modelOk = m_backupSelectionModel.numChecked() > 0;
	m_ui->convertButton->setEnabled(dirOk && modelOk);
}

void DlgConvertExportForMobileApp::transferSelected(void)
{
	const QString baseDir(normalisedDir(m_ui->directoryLine->text(), this));
	if (Q_UNLIKELY(baseDir.isEmpty())) {
		logErrorNL("%s", "Cannot access directory to write a back-up into.");
		return;
	}

	if (Q_UNLIKELY(!isEmptyDir(QDir(baseDir)))) {
		QMessageBox::StandardButton reply = QMessageBox::question(this,
		    tr("Directory not Empty"),
		    tr("The directory '%1' is not empty. Some of the existing data may be overwritten. Do you want to proceed?").arg(baseDir),
		    QMessageBox::No | QMessageBox::Yes, QMessageBox::No);
		if (reply == QMessageBox::No) {
			return;
		}
	}

	/* Create temporary directory for conversions. */
	const QString tmpDir(createSubDirPath(baseDir, "tmp"));
	if (Q_UNLIKELY(tmpDir.isEmpty())) {
		logErrorNL("%s", "Cannot create temporary directory for conversions.");
		return;
	}

	/* Disable user inputs. */
	m_ui->directoryLine->setEnabled(false);
	m_ui->chooseDirButton->setEnabled(false);
	m_ui->allCheckBox->setEnabled(false);
	m_ui->accountView->setEnabled(false);

	m_ui->warningLabel->setEnabled(false);
	m_ui->warningLabel->setVisible(false);
	m_ui->warningLabel->setStyleSheet(QString());
	m_ui->warningLabel->setText(QString());

	m_ui->progressBar->setEnabled(true);
	m_ui->progressBar->setVisible(true);

	m_ui->convertButton->setEnabled(false);

	this->setCursor(Qt::WaitCursor);

	for (const AcntId &acntId : m_backupSelectionModel.checkedAcntIds()) {

		logInfoNL("Attempting to convert data and exporting files for '%s'.",
		    acntId.username().toUtf8().constData());

		MessageDbSet *scrDbSet =
		    GuiHelper::getDbSet(m_acntIdDbList, acntId);
		if (Q_UNLIKELY(scrDbSet == Q_NULLPTR)) {
			m_requiredSpace = SPACE_UNKNOWN;
			Q_ASSERT(0);
			goto finish;
		}

		if (!exportZfoConvertMsgDb(scrDbSet, acntId, tmpDir)) {
			m_ui->warningLabel->setEnabled(true);
			m_ui->warningLabel->setVisible(true);
			m_ui->warningLabel->setStyleSheet("QLabel { color: red }");
			m_ui->warningLabel->setText(
			    tr("Couldn't process database for data box %1 (%2).")
			        .arg(macroAccountName(acntId))
			        .arg(acntId.username()));
			logErrorNL("Cannot export message database for data box '%s'.",
			    acntId.username().toUtf8().constData());
			goto finish;
		}

		logInfoNL("Finished converting data and exporting files for '%s'.",
		    acntId.username().toUtf8().constData());
	}

	{
		QString zipPath = createZipArchive(baseDir, tmpDir);
		if (!zipPath.isEmpty()) {
			logInfoNL("%s", "ZIP archive has been created.");
		}
	}

finish:
	/* Delete temporary directory. */
	removeRecursivelyDir(tmpDir);

	/* Enable user inputs. */
	m_ui->directoryLine->setEnabled(true);
	m_ui->chooseDirButton->setEnabled(true);
	m_ui->allCheckBox->setEnabled(true);
	m_ui->accountView->setEnabled(true);

	/* Leave last message in warningLabel. */

	m_ui->progressBar->setEnabled(false);
	m_ui->progressBar->setVisible(false);

	enableOkButton();

	this->setCursor(Qt::ArrowCursor);
}

void DlgConvertExportForMobileApp::computeRequiredSpace(void)
{
	m_requiredSpace = 0;

	/*
	 * Computing the total size may take a while if the databases need
	 * to be opened first.
	 */
	QApplication::setOverrideCursor(Qt::WaitCursor);

	if (m_backupSelectionModel.numChecked() > 0) {
		for (const AcntId &acntId : m_backupSelectionModel.checkedAcntIds()) {
			MessageDbSet *dbSet =
			    GuiHelper::getDbSet(m_acntIdDbList, acntId);
			if (Q_UNLIKELY(dbSet == Q_NULLPTR)) {
				m_requiredSpace = SPACE_UNKNOWN;
				Q_ASSERT(0);
				break;
			}
			qint64 size = dbSet->dbSize(true);
			if (size >= 0) {
				m_requiredSpace += size;
			} else {
				m_requiredSpace = SPACE_UNKNOWN;
				Q_ASSERT(0);
				break;
			}
		}
	}

	QApplication::restoreOverrideCursor();

	m_ui->requiredSpaceLabel->setText(tr(
	    "Required space to transfer data: %1").arg(Utility::sizeString(m_requiredSpace)));

	showSizeNotificationIfNeeded();
}

void DlgConvertExportForMobileApp::showSizeNotificationIfNeeded(void)
{
	/* Assume at least for a 10 MB margin to stay on safe side. */
	qint64 required = m_requiredSpace + 10485760;

	if ((m_availableSpace != SPACE_UNKNOWN) &&
	    (m_requiredSpace != SPACE_UNKNOWN) &&
	    (m_availableSpace < required)) {
		m_ui->warningLabel->setEnabled(true);
		m_ui->warningLabel->setVisible(true);
		m_ui->warningLabel->setStyleSheet("QLabel { color: red }");
		m_ui->warningLabel->setText(tr("It is likely that the target volume has not enough free space left."));
	} else {
		m_ui->warningLabel->setEnabled(false);
		m_ui->warningLabel->setVisible(false);
		m_ui->warningLabel->setStyleSheet(QString());
		m_ui->warningLabel->setText(QString());
	}
}

void DlgConvertExportForMobileApp::initDialogue(void)
{
	m_ui->infoLabel->setText(
	    tr("Specify the location where you want to export your data and select what data you want to transfer. Selected data will be written into the specified location."));

	m_ui->availableSpaceLabel->setText(QString());
	m_ui->directoryLine->setReadOnly(true);

	m_ui->allCheckBox->setCheckState(Qt::Unchecked);
	m_ui->allCheckBox->setTristate(false);

	m_ui->convertButton->setEnabled(false);

	m_ui->requiredSpaceLabel->setText(QString());
	m_ui->accountView->setNarrowedLineHeight();
	m_ui->accountView->horizontalHeader()->setDefaultAlignment(Qt::AlignVCenter | Qt::AlignLeft);
	m_ui->accountView->setSelectionMode(QAbstractItemView::ExtendedSelection);
	m_ui->accountView->setSelectionBehavior(QAbstractItemView::SelectRows);

	m_ui->accountView->installEventFilter(
	    new (::std::nothrow) TableHomeEndFilter(m_ui->accountView));
	m_ui->accountView->installEventFilter(
	    new (::std::nothrow) TableSpaceSelectionFilter(
	        BackupSelectionModel::COL_MSGS_CHECKBOX, m_ui->accountView));
	m_ui->accountView->installEventFilter(
	    new (::std::nothrow) TableTabIgnoreFilter(m_ui->accountView));

	m_ui->accountView->setModel(&m_backupSelectionModel);

	m_ui->accountView->setSquareColumnWidth(BackupSelectionModel::COL_MSGS_CHECKBOX);
	m_ui->accountView->setColumnHidden(BackupSelectionModel::COL_TESTING, true);

	m_ui->warningLabel->setEnabled(false);
	m_ui->warningLabel->setVisible(false);
	m_ui->warningLabel->setStyleSheet(QString());
	m_ui->warningLabel->setText(QString());

	m_ui->progressBar->setEnabled(false);
	m_ui->progressBar->setVisible(false);

	connect(m_ui->directoryLine, SIGNAL(textChanged(QString)),
	    this, SLOT(computeAvailableSpace(QString)));
	connect(m_ui->directoryLine, SIGNAL(textChanged(QString)),
	    this, SLOT(enableOkButton()));
	connect(m_ui->chooseDirButton, SIGNAL(clicked()),
	    this, SLOT(chooseDirectory()));
	connect(m_ui->allCheckBox, SIGNAL(stateChanged(int)),
	    this, SLOT(applyAllSelection(int)));
	connect(&m_backupSelectionModel,
	    SIGNAL(dataChanged(QModelIndex, QModelIndex)),
	    this, SLOT(modifiedSelection()));
	connect(&m_backupSelectionModel,
	    SIGNAL(dataChanged(QModelIndex, QModelIndex)),
	    this, SLOT(enableOkButton()));

	/* Signal rejected() is connected in the ui file. */
	connect(m_ui->convertButton, SIGNAL(clicked()),
	    this, SLOT(transferSelected()));
}

bool DlgConvertExportForMobileApp::exportZfoConvertMsgDb(MessageDbSet *scrDbSet,
    const AcntId &acntId, const QString &dstDir)
{
	bool ret = true;
	QStringList years = scrDbSet->msgsYears(MessageDb::TYPE_RECEIVED,
	    DESCENDING);
	years.append(scrDbSet->msgsYears(MessageDb::TYPE_SENT,
	    DESCENDING));
	years.removeDuplicates();

	/* Create new single message database. */
	MessageDbSet *dstDbSet = MessageDbSet::createNew(dstDir,
	    acntId.username(), acntId.testing(),
	    MessageDbSet::DO_SINGLE_FILE, true, "NEW_MESSAGE_DB",
	    MessageDbSet::CM_CREATE_EMPTY_CURRENT);
	if (Q_UNLIKELY(Q_NULLPTR == dstDbSet)) {
		logErrorNL("Cannot open new message database file '%s'.",
		    dstDir.toUtf8().constData());
		Q_ASSERT(0);
		return false;
	}

	QString dbName(acntId.username() % DB_UNDERSCORES %
	    (acntId.testing() ? DB_MARKER_TEST : DB_MARKER_PROD) % DB_SUFFIX);
	QString dstMsgDb(joinDirs(dstDir, dbName));

	/* Initialise progressbar for conversion. */
	m_ui->progressBar->setMinimum(0);
	m_ui->progressBar->setMaximum(years.count());
	int pbValue = m_ui->progressBar->minimum();

	m_ui->warningLabel->setEnabled(true);
	m_ui->warningLabel->setVisible(true);
	m_ui->warningLabel->setStyleSheet(QString());
	m_ui->warningLabel->setText(
	    tr("Processing databases and exporting files for data box %1 (%2).")
	        .arg(macroAccountName(acntId))
	        .arg(acntId.username()));

	for (const QString &year : years) {

		QDateTime dateTime;
		dateTime.setDate(QDate(year.toInt(), 1,1));

		MessageDb *msgDb = scrDbSet->accessMessageDb(dateTime, false);
		if (Q_UNLIKELY(Q_NULLPTR == msgDb)) {
			Q_ASSERT(0);
			return false;
		}

		m_ui->progressBar->setValue(pbValue++);
		QCoreApplication::processEvents();

		ret = ret && exportMsg(msgDb, acntId, MessageDb::TYPE_RECEIVED, dstDir);
		ret = ret && exportMsg(msgDb, acntId, MessageDb::TYPE_SENT, dstDir);
		ret = ret && exportDelInfo(msgDb, acntId, dstDir);
		ret = ret && msgDb->copyMessageDataToSingleDb(dstMsgDb);
	}

	dstDbSet->vacuum();
	delete dstDbSet;

	return ret;
}

QString DlgConvertExportForMobileApp::createZipArchive(const QString &zipDir,
    const QString &workDir)
{
	QDir sDirPath(workDir);
	QStringList fileList = recurseAddDir(sDirPath.absolutePath());
	qint64 fileCnt = fileList.count();

	/* Create ZIP archive destination path. */
	const QDateTime currentDateTime = QDateTime::currentDateTime();
	const QString dirName(QStringLiteral(APP_NAME) % QStringLiteral("-") %
	    QStringLiteral("mobile") % QStringLiteral("_transfer_") %
	    currentDateTime.toString(Qt::ISODate).replace(":", "-") %  QStringLiteral(".zip"));

	/* Create ZIP file. */
	QString zipFilePath = joinDirs(zipDir, dirName);
	QuaZip zipFile(zipFilePath);
	if (Q_UNLIKELY(!zipFile.open(QuaZip::mdCreate))) {
		logErrorNL("Cannot create ZIP file in location '%s'.",
		    zipFilePath.toUtf8().constData());
		return QString();
	}

	QuaZipFile quaZipFile(&zipFile);

	/* Initialise progressbar for conversion. */
	m_ui->progressBar->setMinimum(0);
	m_ui->progressBar->setMaximum(fileCnt);

	m_ui->warningLabel->setEnabled(true);
	m_ui->warningLabel->setVisible(true);
	m_ui->warningLabel->setStyleSheet(QString());
	m_ui->warningLabel->setText(tr("Creating transfer ZIP archive."));

	int pbValue = m_ui->progressBar->minimum();

	QList<Json::Backup::File> files;
	for (const QString &file : fileList) {

		m_ui->progressBar->setValue(pbValue++);
		QCoreApplication::processEvents();

		QFileInfo fileInfo(file);
		if (!fileInfo.isFile()) {
			continue;
		}

		if (Q_UNLIKELY(!addFileIntoZip(fileInfo, sDirPath,
		        QString(), quaZipFile))) {
			logErrorNL("File '%s' has not been added into ZIP archive.",
			    fileInfo.fileName().toUtf8().constData());
			return QString();
		}

		Json::Backup::File jsonData(fileNameWithRelativePath(fileInfo,
		    sDirPath, QString()),
		    computeFileChecksum(fileInfo.filePath()), fileInfo.size());
		files.append(jsonData);

		QFile(file).remove();
	}

	/* Create and store transfer JSON file. */
	const QString jsonName("transfer_description.json");
	const QByteArray jsonData(createTransferJson(files));
	if (Q_UNLIKELY(!addJsonIntoZip(jsonName, jsonData, quaZipFile))) {
		logErrorNL("File '%s' has not been added into ZIP archive.",
		    jsonName.toUtf8().constData());
		return QString();
	}

	/* Close ZIP file. */
	zipFile.close();

	m_ui->progressBar->setValue(fileCnt);
	m_ui->warningLabel->setEnabled(true);
	m_ui->warningLabel->setVisible(true);
	m_ui->warningLabel->setStyleSheet(QString());
	m_ui->warningLabel->setText(
	    tr("Transfer ZIP archive has been created:\n%1")
	        .arg(QFileInfo(zipFilePath).fileName()));
	logInfoNL(
	    "All application data have been successfully written into transfer ZIP file '%s'.",
	    zipFilePath.toUtf8().constData());

	return zipFilePath;
}
