/*
 * 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 <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/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_db.h"
#include "src/settings/accounts.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)))

/*!
 * @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)
{
	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);
	dlg.exec();
}

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

/*!
 * @brief Get list of all account identifiers of selected accounts.
 *
 * @param[in] model Account model.
 * @return List of account identifiers.
 */
static
QList<AcntId> selectedAccountIds(const BackupSelectionModel &model)
{
	QList<AcntId> accountIds;

	for (int row = 0; row < model.rowCount(); ++row) {
		QModelIndex idx(
		    model.index(row, BackupSelectionModel::COL_MSGS_CHECKBOX));
		if (idx.data(Qt::CheckStateRole) == Qt::Unchecked) {
			continue;
		}

		const QString username =
		    model.index(row, BackupSelectionModel::COL_USERNAME)
		        .data(Qt::DisplayRole).toString();
		bool testing = model.index(row, BackupSelectionModel::COL_TESTING)
		        .data(Qt::DisplayRole).toBool();
		accountIds.append(AcntId(username, testing));
	}

	return accountIds;
}

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);
		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);
		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->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;
	}

	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->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,
	         selectedAccountIds(m_backupSelectionModel)) {
		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->checkState() == Qt::Checked) {
		QString fileName(GlobInstcs::tagDbPtr->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,
		         selectedAccountIds(m_backupSelectionModel)) {
			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();
			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->checkState() == Qt::Checked)) {
		qint64 size = GlobInstcs::tagDbPtr->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);

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