/*
 * 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 <QDir>
#if (QT_VERSION < QT_VERSION_CHECK(5, 9, 0))
#include <QDirIterator>
#endif /* < Qt-5.9 */
#include <QFileDialog>
#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 <QtGlobal> /* Q_INT64_C */

#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_backup.h"
#include "src/gui/dlg_backup_internal.h"
#include "src/gui/dlg_backup_progress.h"
#include "src/gui/helper.h"
#include "src/io/account_db.h"
#include "src/io/message_db_set.h"
#include "src/io/tag_container.h"
#include "src/io/tag_db.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_backup.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("backup");
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)
{
	foreach (const AcntIdDb &acntIdDb, acntIdDbList) {
		const AcntId acntId(acntIdDb);
		model.appendData(false, macroAccountName(acntId), acntId,
		    macroDataBoxId(acntIdDb.username()));
	}
}

DlgBackup::DlgBackup(const QList<AcntIdDb> &acntIdDbList, QWidget *parent)
    : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgBackup),
    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();
}

DlgBackup::~DlgBackup(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 DlgBackup::backup(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;
	}

	DlgBackup 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 DlgBackup::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 DlgBackup::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 DlgBackup::chooseDirectory(void)
{
	QString dirPath(QFileDialog::getExistingDirectory(this,
	    tr("Open Directory"), QString(),
	    QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks));
	if (!dirPath.isEmpty()) {
		m_ui->directoryLine->setText(QDir::toNativeSeparators(dirPath));
	}
}

void DlgBackup::applyAllSelection(int state)
{
	/* Temporarily disconnect actions which may trigger this slot. */
	m_backupSelectionModel.disconnect(
	    SIGNAL(dataChanged(QModelIndex, QModelIndex)),
	    this, SLOT(modifiedSelection()));
	m_ui->tagsCheckBox->disconnect(SIGNAL(stateChanged(int)),
	    this, SLOT(modifiedSelection()));

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

	/* Reconnect actions. */
	connect(&m_backupSelectionModel,
	    SIGNAL(dataChanged(QModelIndex, QModelIndex)),
	    this, SLOT(modifiedSelection()));
	connect(m_ui->tagsCheckBox, SIGNAL(stateChanged(int)),
	    this, SLOT(modifiedSelection()));

	computeRequiredSpace();
}

void DlgBackup::modifiedSelection(void)
{
	enum Qt::CheckState state = Qt::PartiallyChecked;

	if (m_ui->tagsCheckBox->isEnabled()) {
		if ((m_ui->tagsCheckBox->checkState() == Qt::Checked) &&
		    ((m_backupSelectionModel.rowCount() == 0) ||
		     (m_backupSelectionModel.numChecked() == m_backupSelectionModel.rowCount()))) {
			state = Qt::Checked;
		} else if ((m_ui->tagsCheckBox->checkState() == Qt::Unchecked) &&
		    (m_backupSelectionModel.numChecked() == 0)) {
			state = Qt::Unchecked;
		}
	} else {
		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 DlgBackup::enableOkButton(void)
{
	bool dirOk = !m_ui->directoryLine->text().isEmpty();
	bool modelOk = m_backupSelectionModel.numChecked() > 0;
	bool tagsOk = m_ui->tagsCheckBox->isEnabled() &&
	    (m_ui->tagsCheckBox->checkState() == Qt::Checked);

	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
	    dirOk && (modelOk || tagsOk));
}

/*!
 * @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,
			    DlgBackup::tr("Not a directory"),
			    DlgBackup::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,
		    DlgBackup::tr("Non-existent directory"),
		    DlgBackup::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,
			    DlgBackup::tr("Cannot create directory"),
			    DlgBackup::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,
		    DlgBackup::tr("Cannot write into directory"),
		    DlgBackup::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 */
}

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

	if (!isEmptyDir(QDir(normDir))) {
		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(normDir),
		    QMessageBox::No | QMessageBox::Yes, QMessageBox::No);
		if (reply == QMessageBox::No) {
			return;
		}
	}

	QList<BackupWorker::MessageEntry> msgDbSets;
	QString accountDbPath;
	QString tagDbPath;

	foreach (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;
		}

		const QString boxId(macroDataBoxId(acntId.username()));
		const QString subdir(boxId + QStringLiteral("_") + acntId.username());
		QDir dir(normDir + QStringLiteral("/") + subdir);
		if ((!dir.exists()) && (!dir.mkpath("."))) {
			logErrorNL(
			    "Cannot create or access directory '%s'.",
			    dir.absolutePath().toUtf8().constData());
			QMessageBox::warning(this,
			    tr("Cannot access directory"),
			    tr("Cannot access directory '%1'.")
			        .arg(dir.absolutePath()),
			    QMessageBox::Ok, QMessageBox::Ok);
			continue; /* TODO - Summary. */
		}

		msgDbSets.append(BackupWorker::MessageEntry(
		    dbSet, subdir, macroAccountName(acntId), boxId, acntId));
	}

	{
		QString fileName(GlobInstcs::accntDbPtr->fileName());
		if (Q_UNLIKELY(fileName != SQLiteDb::memoryLocation)) {
			/* Account database is not stored into memory. */
			accountDbPath = QFileInfo(fileName).fileName();
		} else {
			Q_ASSERT(0);
		}
	}

	if (m_ui->tagsCheckBox->isEnabled() &&
	    (m_ui->tagsCheckBox->checkState() == Qt::Checked)) {
		QString fileName(GlobInstcs::tagContPtr->backendDb()->fileName());
		if (Q_UNLIKELY(fileName != SQLiteDb::memoryLocation)) {
			/* Tags are not stored into memory. */
			tagDbPath = QFileInfo(fileName).fileName();
		} else {
			Q_ASSERT(0);
		}
	}

	setEnabled(false);
	int ret = DlgBackupProgress::backup(normDir, msgDbSets, accountDbPath,
	    tagDbPath, this);
	setEnabled(true);

	if (ret) {
		accept(); /* Close dialogue. */
	}
}

void DlgBackup::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) {
		foreach (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;
			}
			/*!
			 * Force access to all databases in the database set.
			 *
			 * FIXME - The code doesn't handle simultaneous backup
			 * and connecting of databases on demand. Forcing the
			 * databases to be open before backing them up is just
			 * a workaround which should be fixed properly.
			 */
			qint64 size = dbSet->dbSize(true);
			if (size >= 0) {
				m_requiredSpace += size;
			} else {
				m_requiredSpace = SPACE_UNKNOWN;
				Q_ASSERT(0);
				break;
			}
		}
	}

	QApplication::restoreOverrideCursor();

	if ((m_requiredSpace != SPACE_UNKNOWN) &&
	    m_ui->tagsCheckBox->isEnabled() &&
	    (m_ui->tagsCheckBox->checkState() == Qt::Checked)) {
		qint64 size = GlobInstcs::tagContPtr->backendDb()->dbSize();
		if (size >= 0) {
			m_requiredSpace += size;
		} else {
			m_requiredSpace = SPACE_UNKNOWN;
			Q_ASSERT(0);
		}
	}

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

	showSizeNotificationIfNeeded();
}

void DlgBackup::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 DlgBackup::initDialogue(void)
{
	m_ui->infoLabel->setText(
	    tr("Specify the location where you want to back up your data and select what data you want to back up. Selected data will be written into the specified location.") +
	    QStringLiteral("\n") +
	    tr("Don't modify any of the written files unless you really know what you are doing.") +
	    QStringLiteral(" ") +
	    tr("Preserve the back-up in a safe place as it contains private data."));

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

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

	m_ui->requiredSpaceLabel->setText(QString());
	m_ui->accountView->setNarrowedLineHeight();
	m_ui->accountView->horizontalHeader()->setDefaultAlignment(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);

	/* Enable tag backups only for local database. */
	m_ui->tagsCheckBox->setEnabled(GlobInstcs::tagContPtr->backend() == TagContainer::BACK_DB);
	m_ui->tagsCheckBox->setCheckState(Qt::Unchecked);

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

	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()));
	connect(m_ui->tagsCheckBox, SIGNAL(stateChanged(int)),
	    this, SLOT(modifiedSelection()));
	connect(m_ui->tagsCheckBox, SIGNAL(stateChanged(int)),
	    this, SLOT(enableOkButton()));

	/* Signal rejected() is connected in the ui file. */
	connect(m_ui->buttonBox, SIGNAL(accepted()),
	    this, SLOT(backupSelected()));
}
