/*
 * 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 <QCloseEvent>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QStandardPaths>
#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 "src/common.h"
#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/datovka_shared/log/log.h"
#include "src/db_repair/gui/mw_db_repair.h"
#include "src/db_repair/io/corrupt_message_db.h"
#include "src/global.h" /* GlobInstcs::req_halt */
#include "src/gui/dlg_msg_box_detail.h"
#include "src/isds/message_functions.h" /* Isds::messageFromFile() */
#include "ui_mw_db_repair.h"

bool MWDbRepair::runDatabaseRepair = false;
QString MWDbRepair::passedMessageDbDir;
QString MWDbRepair::passedMessageDbFile;

static
volatile int req_close = 0; /*!< Set to 1 if window close requested. */

RepairThread::RepairThread(void)
    : QThread(),
    m_srcDbPath(),
    m_workDirPath(),
    m_reassembleDb(false),
    m_tgtDbPath()
{
}

void RepairThread::setSrcDbPath(const QString &srcDbPath)
{
	m_srcDbPath = srcDbPath;
}

void RepairThread::setWorkDirPath(const QString &workDirPath)
{
	m_workDirPath = workDirPath;
}

void RepairThread::setReassembleDb(bool reassembleDb)
{
	m_reassembleDb = reassembleDb;
}

const QString &RepairThread::tgtDbPath(void) const
{
	return m_tgtDbPath;
}

/*!
 * @brief Create the file name for a data message file with given ID.
 *
 * @param[in] messageDirection Determines received or sent message on unknown
 *                             (enum MessageDirection).
 * @param[in] dmId Message identifier.
 * @param[in] dmVODZ True is high-volume message.
 * @return File name.
 */
static
QString msgZfoFileName(int messageDirection, qint64 dmId, bool dmVODZ)
{
	return QString("%1%2_%3.zfo")
	    .arg((MSG_RECEIVED == messageDirection)
	             ? "D"
	             : ((MSG_SENT == messageDirection ) ? "O" : ""))
	    .arg(dmVODZ ? "VODZ" : "DZ")
	    .arg(dmId);
}

/*!
 * @brief Create the file name for a delivery info file with given ID.
 *
 * @param[in] dmId Message identifier.
 * @return File name.
 */
static
QString delInfoZfoFileName(qint64 dmId)
{
	return QString("DD_%1.zfo").arg(dmId);
}

/*!
 * @brief Create file and write data to it.
 *
 * @Note target file will be deleted in any error.
 *
 * @param[in] filePath File path.
 * @param[in] data Data to be written into file.
 * @param[out] errStr Error string.
 * @return True on success.
 */
static
bool writeFile(const QString &filePath, const QByteArray &data, QString &errStr)
{
	if (Q_UNLIKELY(filePath.isEmpty())) {
		Q_ASSERT(0);
		return false;
	}

	QFile fout(filePath);
	if (Q_UNLIKELY(!fout.open(QIODevice::WriteOnly))) {
		errStr = fout.errorString();
		return false;
	}

	const qint64 written = fout.write(data);
	if (Q_UNLIKELY(written < 0)) {
		errStr = fout.errorString();
	}
	const bool flushed = fout.flush();
	fout.close();

	if (Q_UNLIKELY((written != data.size()) || (!flushed))) {
		/* Delete on error. */
		QFile::remove(filePath);
		return false;
	}

	return true;
}

void RepairThread::run(void)
{
	m_tgtDbPath.clear();

	if (Q_UNLIKELY(m_srcDbPath.isEmpty())) {
		logErrorNL("%s", "Missing source message database.");
		Q_EMIT errorMsg(tr("Missing source message database."));
		return;
	}

	if (Q_UNLIKELY(m_workDirPath.isEmpty())) {
		logErrorNL("%s", "Work directory is not specified.");
		Q_EMIT errorMsg(tr("Work directory is not specified."));
		return;
	}

	CorruptMessageDb cmdb("CORRUPT_MESSAGE_DATABASE");
	if (Q_UNLIKELY(!cmdb.openDb(m_srcDbPath))) {
		logErrorNL("File '%s' cannot be opened as a database.",
		    m_srcDbPath.toUtf8().constData());
		Q_EMIT errorMsg(tr("File '%1' cannot be opened as a database.")
		        .arg(QDir::toNativeSeparators(m_srcDbPath)));
		return;
	}

	if (Q_UNLIKELY(!cmdb.containsKnownTables())) {
		logErrorNL("File '%s' is either not a message database or doesn't contain an SQLite database at all.",
		    m_srcDbPath.toUtf8().constData());
		Q_EMIT errorMsg(tr("File '%1' is either not a message database or doesn't contain an SQLite database at all.")
		        .arg(QDir::toNativeSeparators( m_srcDbPath)));
		return;
	}

	if (Q_UNLIKELY(isInterruptionRequested())) {
		return;
	}

	/* Collect message identifiers in message envelope tables. */
	QSet<qint64> msgsDmIds;
	cmdb.collectDmIdsFromMsgTable(msgsDmIds);

	Q_EMIT infoMsg(tr("Found entries of %n message envelope(s).", "", msgsDmIds.size()));

	/* Collect message identifiers in raw message data tables. */
	QSet<qint64> rawMsgReceivedDmIds;
	QSet<qint64> rawMsgSentDmIds;
	QSet<qint64> rawMsgUnknownDmIds;

	cmdb.collectDmIdsFromRawMsgTable(rawMsgReceivedDmIds, rawMsgSentDmIds,
	    rawMsgUnknownDmIds);

	Q_EMIT infoMsg(tr("Found entries of %n received message(s).", "", rawMsgReceivedDmIds.size()));
	Q_EMIT infoMsg(tr("Found entries of %n sent message(s).", "", rawMsgSentDmIds.size()));
	Q_EMIT infoMsg(tr("Found entries of %n unidentified message(s).", "", rawMsgUnknownDmIds.size()));

	/* Collect message identifiers in raw delivery info tables. */
	QSet<qint64> rawDelInfoDmIds;

	cmdb.collectDmIdsFromRawDelInfoTable(rawDelInfoDmIds);

	Q_EMIT infoMsg(tr("Found entries of %n delivery info(s).", "", rawDelInfoDmIds.size()));

	if (Q_UNLIKELY(isInterruptionRequested())) {
		return;
	}

	/* Collect message identifiers in supplementary message data tables. */
	QSet<qint64> supplementaryReceivedDmIds;
	QSet<qint64> supplementarySentDmIds;
	QSet<qint64> supplementaryUnknownDmIds;

	cmdb.collectDmIdsFromSupplementary(supplementaryReceivedDmIds,
	    supplementarySentDmIds, supplementaryUnknownDmIds);

	Q_EMIT infoMsg(tr("Found entries of %n received message(s) in supplementary table.", "", supplementaryReceivedDmIds.size()));
	Q_EMIT infoMsg(tr("Found entries of %n sent message(s) in supplementary table.", "", supplementarySentDmIds.size()));
	Q_EMIT infoMsg(tr("Found entries of %n unidentified message(s) in supplementary table.", "", supplementaryUnknownDmIds.size()));

	QSet<qint64> supplementaryVodzDmIds;
	const bool knowsVODZ = cmdb.knowsDmVodz();

	if (knowsVODZ) {
		cmdb.collectDmIdsFromSupplementaryVODZ(supplementaryVodzDmIds);
	}

	Q_EMIT infoMsg(tr("Found entries of %n high-volume message(s) in supplementary table.", "", supplementaryVodzDmIds.size()));

	if (Q_UNLIKELY(isInterruptionRequested())) {
		return;
	}

	/* Compute overall messages in database. */
	QSet<qint64> receivedDmIds = rawMsgReceivedDmIds;
	receivedDmIds.unite(supplementaryReceivedDmIds);
	QSet<qint64> sentDmIds = rawMsgSentDmIds;
	sentDmIds.unite(supplementarySentDmIds);
	QSet<qint64> unknownDmIds = msgsDmIds;
	unknownDmIds.unite(rawMsgUnknownDmIds);
	unknownDmIds.unite(rawDelInfoDmIds);
	unknownDmIds.unite(supplementaryUnknownDmIds);
	unknownDmIds.subtract(receivedDmIds);
	unknownDmIds.subtract(sentDmIds);

	/* Check whether some messages aren't in both received and sent sets. */
	{
		QSet<qint64> both = receivedDmIds;
		both.intersect(sentDmIds);

		if (Q_UNLIKELY(!both.isEmpty())) {
			Q_EMIT errorMsg(tr("Found conflicting entries for %n message(s).", "", both.size()));

			/* Adjust conflicting data. */
			receivedDmIds.subtract(both);
			sentDmIds.subtract(both);
			unknownDmIds.unite(both);
		}
	}

	/* Stored files. */
	QMap<qint64, QString> msgFiles;
	QMap<qint64, QString> delInfoFiles;

	/* Extract message ZFO data and save to files. */
	for (const qint64 dmId : receivedDmIds) {
		if (Q_UNLIKELY(isInterruptionRequested())) {
			return;
		}

		QString filePath = extracttMessageFile(cmdb, dmId,
		    MSG_RECEIVED, supplementaryVodzDmIds.contains(dmId));
		if (!filePath.isEmpty()) {
			msgFiles[dmId] = macroStdMove(filePath);
		}
	}
	for (const qint64 dmId : sentDmIds) {
		if (Q_UNLIKELY(isInterruptionRequested())) {
			return;
		}

		QString filePath = extracttMessageFile(cmdb, dmId,
		    MSG_SENT, supplementaryVodzDmIds.contains(dmId));
		if (!filePath.isEmpty()) {
			msgFiles[dmId] = macroStdMove(filePath);
		}
	}
	for (const qint64 dmId : unknownDmIds) {
		if (Q_UNLIKELY(isInterruptionRequested())) {
			return;
		}

		QString filePath = extracttMessageFile(cmdb, dmId,
		    MSG_ALL, supplementaryVodzDmIds.contains(dmId));
		if (!filePath.isEmpty()) {
			msgFiles[dmId] = macroStdMove(filePath);
		}
	}

	/* Extract delivery info ZFO data and save to files. */
	{
		/* Try every found message identifier. */
		QSet<qint64> dmIds = rawDelInfoDmIds;
		dmIds.unite(receivedDmIds);
		dmIds.unite(sentDmIds);
		dmIds.unite(unknownDmIds);

		for (const qint64 dmId : dmIds) {
			if (Q_UNLIKELY(isInterruptionRequested())) {
				return;
			}

			const QString filePath = m_workDirPath % QStringLiteral("/") % delInfoZfoFileName(dmId);
			const QByteArray data = cmdb.getDeliveryInfoRawDirect(dmId);
			if (!data.isEmpty()) {
				QString errStr;
				if (writeFile(filePath, data, errStr)) {
					Q_EMIT infoMsg(tr("Saved delivery info file '%1'.").arg(QDir::toNativeSeparators(filePath)));
					delInfoFiles[dmId] = filePath;
				} else {
					Q_EMIT errorMsg(tr("Cannot save delivery info file '%1': %2")
					    .arg(QDir::toNativeSeparators(filePath)
					    .arg(errStr)));
				}
			} else {
				Q_EMIT infoMsg(tr("Missing delivery info data for delivery info '%1'.").arg(dmId));
			}
		}
	}

	if (Q_UNLIKELY(isInterruptionRequested())) {
		return;
	}

	if (!m_reassembleDb) {
		return;
	}

	/* Try reassembling message database. */
	{
		QMap<qint64, Isds::Envelope> receivedEnvelopes;
		QMap<qint64, Isds::Envelope> sentEnvelopes;
		QMap<qint64, Isds::Envelope> unknownEnvelopes;

		receivedEnvelopes = collectEnvelopes(cmdb, receivedDmIds, knowsVODZ);
		sentEnvelopes = collectEnvelopes(cmdb, sentDmIds, knowsVODZ);
		unknownEnvelopes = collectEnvelopes(cmdb, unknownDmIds, knowsVODZ);

		if (receivedEnvelopes.isEmpty() && sentEnvelopes.isEmpty()) {
			/* Nothing to restore. */
			Q_EMIT errorMsg(tr("No reliable envelope data could be read from original database file. Won't attempt any reassembling."));
			return;
		}

		/*
		 * Delete conflicting database file if found and create new one.
		 */
		QString tgtDbFileName = "restored_" + QFileInfo(m_srcDbPath).fileName();
		QString tgtDbPath = m_workDirPath % QStringLiteral("/") % tgtDbFileName;
		QFile::remove(tgtDbPath);

		CorruptMessageDb tgtDb("RESTORED_MESSAGE_DATABASE");
		if (Q_UNLIKELY(tgtDb.openDb(tgtDbPath, SQLiteDb::CREATE_MISSING))) {
			Q_EMIT infoMsg(tr("Created database file '%1'.").arg(tgtDbPath));
		} else {
			logErrorNL("File '%s' cannot be opened as a database.",
			    tgtDbPath.toUtf8().constData());
			Q_EMIT errorMsg(tr("File '%1' cannot be opened as a database.")
			        .arg(QDir::toNativeSeparators(tgtDbPath)));
			return;
		}

		QSet<qint64> insertedReceivedEnvelopeIds;
		QSet<qint64> insertedSentEnvelopeIds;

		if (Q_UNLIKELY(isInterruptionRequested())) {
			return;
		}
		if (!receivedEnvelopes.isEmpty()) {
			insertedReceivedEnvelopeIds = reinsertEnvelopes(tgtDb,
			    receivedEnvelopes, MessageDb::TYPE_RECEIVED);
		}
		if (!sentEnvelopes.isEmpty()) {
			insertedSentEnvelopeIds = reinsertEnvelopes(tgtDb,
			    sentEnvelopes, MessageDb::TYPE_SENT);
		}

		for (const qint64 dmId : msgFiles.keys()) {
			if (Q_UNLIKELY(isInterruptionRequested())) {
				return;
			}

			if (receivedDmIds.contains(dmId)) {
				reinsertMessage(tgtDb, msgFiles.value(dmId),
				    MessageDb::TYPE_RECEIVED);
			} else if (sentDmIds.contains(dmId)) {
				reinsertMessage(tgtDb, msgFiles.value(dmId),
				    MessageDb::TYPE_SENT);
			}
		}

		for (const qint64 dmId : delInfoFiles.keys()) {
			if (Q_UNLIKELY(isInterruptionRequested())) {
				return;
			}

			if (receivedDmIds.contains(dmId) || sentDmIds.contains(dmId)) {
				reinsertDeliveryInfo(tgtDb,
				    delInfoFiles.value(dmId));
			}
		}

		m_tgtDbPath = tgtDbPath;

		Q_EMIT infoMsg(tr("A message database file '%1' has been formed from recovered data.")
		    .arg(QDir::toNativeSeparators(tgtDbPath)));
	}

	return;
}

QString RepairThread::extracttMessageFile(CorruptMessageDb &cmdb, qint64 dmId,
    int messageDirection, bool dmVODZ)
{
	const QString filePath = m_workDirPath % QStringLiteral("/") % msgZfoFileName(
	    messageDirection, dmId, dmVODZ);
	const QByteArray data = cmdb.getCompleteMessageRawDirect(dmId);
	if (!data.isEmpty()) {
		QString errStr;
		if (writeFile(filePath, data, errStr)) {
			Q_EMIT infoMsg(tr("Saved message file '%1'.").arg(QDir::toNativeSeparators(filePath)));
			return filePath;
		} else {
			Q_EMIT errorMsg(tr("Cannot save message file '%1': %2")
			    .arg(QDir::toNativeSeparators(filePath)
			    .arg(errStr)));
		}
	} else {
		switch (messageDirection) {
		case MSG_RECEIVED:
			Q_EMIT infoMsg(tr("Missing message data for received message '%1'.").arg(dmId));
			break;
		case MSG_SENT:
			Q_EMIT infoMsg(tr("Missing message data for sent message '%1'.").arg(dmId));
			break;
		default: /* Unknown direction. */
			Q_EMIT infoMsg(tr("Missing message data for message '%1'.").arg(dmId));
			break;
		}
	}

	return QString();
}

QMap<qint64, Isds::Envelope> RepairThread::collectEnvelopes(
    CorruptMessageDb &cmdb, const QSet<qint64> &dmIds, bool knowsVODZ)
{
	QMap<qint64, Isds::Envelope> envelopes;

	for (qint64 dmId : dmIds) {
		Isds::Envelope env = knowsVODZ
		    ? cmdb.getMessageEnvelope(dmId)
		    : cmdb.getMessageEnvelopeNoDmVodz(dmId);
		if (!env.isNull()) {
			envelopes[dmId] = macroStdMove(env);
		} else {
			Q_EMIT errorMsg(tr("Cannot read envelope of message '%1'.").arg(dmId));
		}
	}

	return envelopes;
}

/*!
 * @brief Convert enum MessageDb::MessageType to enum MessageDirection.
 *
 * @param[in] messageType Message type.
 * @returns Corresponding MessageDirection value or MSG_ALL if unknown message
 *     type supplied.
 */
static
enum MessageDirection msgType2Direction(int messageType)
{
	switch (messageType) {
	case MessageDb::TYPE_RECEIVED:
		return MSG_RECEIVED;
		break;
	case MessageDb::TYPE_SENT:
		return MSG_SENT;
		break;
	default:
		Q_ASSERT(0);
		return MSG_ALL;
		break;
	}
}

QSet<qint64> RepairThread::reinsertEnvelopes(MessageDb &tgtDb,
    const QMap<qint64, Isds::Envelope> &envelopes, int messageType)
{
	QSet<qint64> inserted;

	const enum MessageDirection direct = msgType2Direction(messageType);
	if (Q_UNLIKELY(MSG_ALL == direct)) {
		return inserted;
	}

	const bool implicitTransaction = false;

	if (!implicitTransaction) {
		if (Q_UNLIKELY(!tgtDb.beginTransaction())) {
			goto fail;
		}
	}

	for (const Isds::Envelope &env : envelopes) {
		if (Q_UNLIKELY(env.isNull())) {
			continue;
		}
		if (tgtDb.insertMessageEnvelope(env, QString(), direct, implicitTransaction)) {
			inserted.insert(env.dmId());
		} else {
			Q_EMIT errorMsg(tr("Cannot insert envelope of message '%1'.").arg(env.dmId()));
		}
	}

	if (!implicitTransaction) {
		tgtDb.commitTransaction();
	}

fail:
	return inserted;
}

bool RepairThread::storeSignedDeliveryInfo(MessageDb &tgtDb,
    const Isds::Message &message, bool storeRawInDb)
{
	bool ret = false;

	if (Q_UNLIKELY(message.isNull())) {
		Q_ASSERT(0);
		return ret;
	}

	const qint64 dmId = message.envelope().dmId();

	/* Get signed raw data from message. */
	if (tgtDb.insertOrReplaceDeliveryInfoRaw(storeRawInDb, dmId,
	        message.raw())) {
		ret = true;
	} else {
		Q_EMIT errorMsg(tr("Cannot insert delivery info of message '%1'.").arg(dmId));
	}

	for (const Isds::Event &event : message.envelope().dmEvents()) {
		tgtDb.insertOrUpdateMessageEvent(dmId, event);
	}

	return ret;
}

bool RepairThread::storeSignedMessage(bool transaction, int messageType,
    MessageDb &tgtDb, const Isds::Message &message, bool storeRawInDb)
{
	if (Q_UNLIKELY(message.isNull())) {
		Q_ASSERT(0);
		return false;
	}

	const enum MessageDirection direct = msgType2Direction(messageType);
	if (Q_UNLIKELY(MSG_ALL == direct)) {
		return false;
	}

	const qint64 dmId = message.envelope().dmId();

	/*
	 * If there is no raw message then all the attachments have been
	 * stored when the message has been set.
	 */
	if (!tgtDb.isCompleteMessageInDb(dmId)) {
		tgtDb.deleteMessageAttachments(dmId);
	}

	bool verified = false;
	{
		/* Verify message signature. */
//		int ret = raw_msg_verify_signature(message.raw(),
//		    message.raw().length(), 1, 0);
//		verified = (1 == ret);
		verified = true;
	}

	if (Q_UNLIKELY(!tgtDb.insertOrReplaceCompleteMessage(storeRawInDb,
	        message, direct, verified, transaction))) {
		return false;
	}

	return true;
}

bool RepairThread::reinsertDeliveryInfo(MessageDb &tgtDb,
    const QString &infoFilePath)
{
	Isds::Message msg = Isds::messageFromFile(infoFilePath,
	     Isds::LT_DELIVERY);
	if (Q_UNLIKELY(msg.isNull())) {
		return false;
	}

	return storeSignedDeliveryInfo(tgtDb, msg, true);
}

bool RepairThread::reinsertMessage(MessageDb &tgtDb, const QString &msgFilePath,
    int messageType)
{
	Isds::Message msg = Isds::messageFromFile(msgFilePath,
	    Isds::LT_MESSAGE);
	if (Q_UNLIKELY(msg.isNull())) {
		return false;
	}

	const bool implicitTransaction = true;

	return storeSignedMessage(implicitTransaction, messageType, tgtDb, msg,
	    true);
}

MWDbRepair::MWDbRepair(QWidget *parent)
    : QMainWindow(parent),
    m_ui(new (::std::nothrow) Ui::MWDbRepair),
    m_timerCheckReqHalt(),
    m_askCloseDlg(Q_NULLPTR),
    m_repairThread(new (::std::nothrow) RepairThread)
{
	m_ui->setupUi(this);

	topMenuConnectActions();

	connect(m_ui->dbFilePath, SIGNAL(textChanged(QString)),
	    this, SLOT(checkInputFields()));
	connect(m_ui->workDirPath, SIGNAL(textChanged(QString)),
	    this, SLOT(checkInputFields()));

	connect(m_ui->workDirButton, SIGNAL(clicked()),
	    this, SLOT(selectWorkDirectory()));

	connect(m_ui->runRecoveryButton, SIGNAL(clicked()),
	    this, SLOT(runRepair()));

	connect(m_ui->replaceCorruptButton, SIGNAL(clicked()),
	    this, SLOT(replaceCorrupt()));

	m_ui->dbFilePath->setEnabled(true);
	m_ui->dbFilePath->setReadOnly(true);

	m_ui->workDirPath->setEnabled(true);
	m_ui->workDirPath->setReadOnly(true);

	m_ui->recoveredDbPath->setEnabled(true);
	m_ui->recoveredDbPath->setReadOnly(true);
	m_ui->copyOriginalDbCheckbox->setCheckState(Qt::Checked);

	m_ui->recoverdGroupBox->setEnabled(false);

	m_ui->repairInfo->setEnabled(true);
	m_ui->repairInfo->setReadOnly(true);

	if (Q_NULLPTR != m_repairThread) {
		connect(m_repairThread, SIGNAL(infoMsg(QString)),
		    this, SLOT(showInfo(QString)),
		    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */
		connect(m_repairThread, SIGNAL(errorMsg(QString)),
		    this, SLOT(showError(QString)),
		    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */

		connect(m_repairThread, SIGNAL(finished()),
		    this, SLOT(finishedRepair()),
		    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */
	}

	checkInputFields();

	/* Initialisation of requested halt checker timer. */
	connect(&m_timerCheckReqHalt, SIGNAL(timeout()),
	    this, SLOT(haltRequested()));
	m_timerCheckReqHalt.start(200);

	if (!passedMessageDbFile.isEmpty()) {
		loadAndCheckMessageDbFile(passedMessageDbFile);
	}
}

MWDbRepair::~MWDbRepair(void)
{
	if (Q_NULLPTR != m_repairThread) {
		if (Q_UNLIKELY(!m_repairThread->wait(500))) {
			m_repairThread->terminate();
			m_repairThread->wait();
		}
		delete m_repairThread; m_repairThread = Q_NULLPTR;
	}

	delete m_ui;
}

void MWDbRepair::closeEvent(QCloseEvent *event)
{
	/*
	 * Check whether any threads are currently being processed or are
	 * pending. If nothing is being processed finish immediately else ask
	 * the user.
	 */
	if (Q_NULLPTR != m_repairThread) {
		if (m_repairThread->isRunning()) {
			if (GlobInstcs::req_halt || req_close) {
				m_repairThread->requestInterruption();
			} else {
				viewAskCloseDlg();
				event->ignore();
			}
		}
	}
}

void MWDbRepair::haltRequested(void)
{
	if (Q_UNLIKELY(GlobInstcs::req_halt)) {
		logInfoNL("%s", "Requested halt.");
		m_repairThread->requestInterruption();
		close(); /* Close this window. */
	}
}

/*!
 * @brief Raise a widget.
 *
 * @param[in,out] widget Widget that should be de-minimised and shown.
 * @return True on success.
 */
static inline
bool raiseWidget(QWidget *widget)
{
	if (Q_UNLIKELY(Q_NULLPTR == widget)) {
		return false;
	}
	if (!widget->isMinimized()) {
		widget->show();
	} else {
		widget->showNormal();
	}
	widget->raise();
	widget->activateWindow();
	return true;
}

void MWDbRepair::viewAskCloseDlg(void)
{
	debugSlotCall();

	if (raiseWidget(m_askCloseDlg)) {
		return;
	}

	m_askCloseDlg = new (::std::nothrow) DlgMsgBoxDetail(
	    QMessageBox::Question, tr("Pending Tasks"),
	    tr("The application is currently processing some tasks."),
	    tr("Do you want to abort all pending actions and close the application?"),
	    QString(), QMessageBox::No | QMessageBox::Yes, QMessageBox::No, this);
	if (Q_UNLIKELY(m_askCloseDlg == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}
	connect(m_askCloseDlg, SIGNAL(finished(int)),
	    this, SLOT(closedAskCloseDlg(int)));
	m_askCloseDlg->show();
}

void MWDbRepair::closedAskCloseDlg(int result)
{
	debugSlotCall();

	m_askCloseDlg->setAttribute(Qt::WA_DeleteOnClose, true);
	m_askCloseDlg->deleteLater();
	m_askCloseDlg = Q_NULLPTR;

	if (result == QMessageBox::Yes) {
		req_close = 1;
		close();
	}
}

void MWDbRepair::loadMessageDbFile(void)
{
	QString openedPath;
	if (!m_ui->dbFilePath->text().isEmpty()) {
		openedPath = QDir::fromNativeSeparators(m_ui->dbFilePath->text());
		if (!openedPath.isEmpty()) {
			openedPath = QFileInfo(openedPath).absoluteDir().absolutePath();
		}
	} else if (!MWDbRepair::passedMessageDbDir.isEmpty()) {
		openedPath = QFileInfo(MWDbRepair::passedMessageDbDir).absoluteFilePath();
	}

	QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"),
	     openedPath, tr("Database file (*.db)"));
	if (Q_UNLIKELY(fileName.isEmpty())) {
		return;
	}

	loadAndCheckMessageDbFile(fileName);
}

void MWDbRepair::selectWorkDirectory(void)
{
	QString dirPath;
	if (!m_ui->workDirPath->text().isEmpty()) {
		dirPath = QDir::fromNativeSeparators(m_ui->workDirPath->text());
	} else {
		dirPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
	}
	dirPath = QFileDialog::getExistingDirectory(this,
	    tr("Select Directory"), dirPath,
	    QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);

	if (Q_UNLIKELY(dirPath.isEmpty())) {
		return;
	}

	{
		QFileInfo fi(dirPath);

		dirPath = fi.absoluteFilePath();

		if (Q_UNLIKELY(!fi.isDir())) {
			QMessageBox::warning(this, tr("Not a Directory"),
			    tr("The path '%1' isn't a directory.")
			        .arg(QDir::toNativeSeparators(dirPath)));
			m_ui->workDirPath->setText(QString());
			return;
		}

		if (Q_UNLIKELY(!fi.isWritable())) {
			QMessageBox::warning(this, tr("Not Writeable"),
			    tr("Directory '%1' cannot be written to.")
			        .arg(QDir::toNativeSeparators(dirPath)));
			m_ui->workDirPath->setText(QString());
			return;
		}
	}

	m_ui->workDirPath->setText(QDir::toNativeSeparators(dirPath));
}

void MWDbRepair::checkInputFields(void)
{
	const bool enabled = (!m_ui->dbFilePath->text().isEmpty())
	    && (!m_ui->workDirPath->text().isEmpty());

	m_ui->reassembleDbCheckBox->setEnabled(enabled);
	m_ui->runRecoveryButton->setEnabled(enabled);
}

void MWDbRepair::showInfo(const QString &str)
{
	m_ui->repairInfo->append(str);
}

void MWDbRepair::showError(const QString &str)
{
	m_ui->repairInfo->append(
	    QString("<div style=\"color:red;\">%1</div>").arg(tr("Error:"))
	        % QStringLiteral(" ") % str);
//	m_ui->repairInfo->ensureCursorVsible();
}

void MWDbRepair::runRepair(void)
{
	if (Q_UNLIKELY(Q_NULLPTR == m_repairThread)) {
		return;
	}

	{
		const bool enabled = false;

		m_ui->actionLoadDatabaseFile->setEnabled(enabled);
		m_ui->dbFilePath->setEnabled(enabled);
		m_ui->workDirPath->setEnabled(enabled);
		m_ui->workDirButton->setEnabled(enabled);
		m_ui->reassembleDbCheckBox->setEnabled(enabled);
		m_ui->runRecoveryButton->setEnabled(enabled);

		m_ui->recoveredDbPath->setText(QString());
		m_ui->recoverdGroupBox->setEnabled(enabled);
	}

	m_repairThread->setSrcDbPath(QDir::fromNativeSeparators(m_ui->dbFilePath->text()));
	m_repairThread->setWorkDirPath(QDir::fromNativeSeparators(m_ui->workDirPath->text()));
	m_repairThread->setReassembleDb(m_ui->reassembleDbCheckBox->checkState() == Qt::Checked);

	this->setCursor(Qt::BusyCursor);

	m_repairThread->start();
}

void MWDbRepair::finishedRepair(void)
{
	this->setCursor(Qt::ArrowCursor);

	showInfo(tr("Finished."));

	{
		const bool enabled = true;

		m_ui->actionLoadDatabaseFile->setEnabled(enabled);
		m_ui->dbFilePath->setEnabled(enabled);
		m_ui->workDirPath->setEnabled(enabled);
		m_ui->workDirButton->setEnabled(enabled);
		m_ui->reassembleDbCheckBox->setEnabled(enabled);
		m_ui->runRecoveryButton->setEnabled(enabled);
	}
	if (Q_NULLPTR != m_repairThread) {
		m_ui->recoveredDbPath->setText(
		    QDir::toNativeSeparators(m_repairThread->tgtDbPath()));
		m_ui->recoverdGroupBox->setEnabled(
		    !m_ui->recoveredDbPath->text().isEmpty());
	}
}

/*!
 * @brief Check for available space on device.
 *
 * @param[in] path Specified the device.
 * @param[in] requiredSpace Amount of required space in bytes.
 * @return True if there is enough available space on de device.
 */
static
bool checkAvailableSpace(const QString &path, qint64 requiredSpace)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
{
	const qint64 availableBytes = QStorageInfo(path).bytesAvailable();
	/* Ignores some available space, just to be safe. */
	return (availableBytes >= 0) && ((qint64)(0.95 * availableBytes) >= requiredSpace);
}
#else /* < Qt-5.4 */
{
	return true;
}
#endif /* >= Qt-5.4 */

void MWDbRepair::replaceCorrupt(void)
{
	const QString corruptFilePath =
	    QDir::fromNativeSeparators(m_ui->dbFilePath->text());
	const QString workDirPath = QDir::fromNativeSeparators(m_ui->workDirPath->text());
	const QString restoredFilePath =
	    QDir::fromNativeSeparators(m_ui->recoveredDbPath->text());

	const bool createCorruptCopy =
	    m_ui->copyOriginalDbCheckbox->checkState() == Qt::Checked;

	if (QMessageBox::Yes != QMessageBox::question(this, tr("Overwrite File?"),
	        tr("Do you want to overwrite the file '%1'?")
	            .arg(QDir::toNativeSeparators(corruptFilePath)),
	        QMessageBox::Yes | QMessageBox::No, QMessageBox::No)) {
		return;
	}

	this->setCursor(Qt::WaitCursor);
	this->setEnabled(false);

	if (createCorruptCopy) {
		if (Q_UNLIKELY(!checkAvailableSpace(workDirPath,
		    QFileInfo(corruptFilePath).size()))) {
			const QString msg = tr("There isn't enough space in directory '%1'.")
			    .arg(QDir::toNativeSeparators(workDirPath));
			showError(msg);
			QMessageBox::warning(this, tr("Not Enough Space"), msg);
			goto fail;
		}

		const QString tgtDbFileName = "corrupt_" + QFileInfo(corruptFilePath).fileName();
		const QString tgtDbPath = workDirPath % QStringLiteral("/") % tgtDbFileName;

		QFile::remove(tgtDbPath);
		if (QFile::copy(corruptFilePath, tgtDbPath)) {
			showInfo(tr("Copied file '%1' to '%2'.")
			    .arg(QDir::toNativeSeparators(corruptFilePath))
			    .arg(QDir::toNativeSeparators(tgtDbPath)));
		} else {
			const QString msg = tr("Cannot copy file '%1' to '%2'.")
			    .arg(QDir::toNativeSeparators(corruptFilePath))
			    .arg(QDir::toNativeSeparators(tgtDbPath));
			showError(msg);
			QMessageBox::warning(this, tr("Copy Error"), msg);
			goto fail;
		}
	}

	/* Replace original file. */
	if (Q_UNLIKELY(!QFile::remove(corruptFilePath))) {
		const QString msg = tr("Cannot delete file '%1'.")
		    .arg(QDir::toNativeSeparators(corruptFilePath));
		showError(msg);
		QMessageBox::warning(this, tr("Deletion Error"), msg);
		goto fail;
	}
	/* Presume that restored file is smaller of same size as original. */
	if (QFile::copy(restoredFilePath, corruptFilePath)) {
		showInfo(tr("Copied file '%1' to '%2'.")
		    .arg(QDir::toNativeSeparators(restoredFilePath))
		    .arg(QDir::toNativeSeparators(corruptFilePath)));
	} else {
		const QString msg = tr("Cannot copy file '%1' to '%2'.")
		    .arg(QDir::toNativeSeparators(restoredFilePath))
		    .arg(QDir::toNativeSeparators(corruptFilePath));
		showError(msg);
		QMessageBox::warning(this, tr("Copy Error"), msg);
		goto fail;
	}

fail:
	this->setEnabled(true);
	this->setCursor(Qt::ArrowCursor);
}

void MWDbRepair::loadAndCheckMessageDbFile(QString fileName)
{
	{
		QFileInfo fi(fileName);

		fileName = fi.absoluteFilePath();

		if (Q_UNLIKELY(!fi.isFile())) {
			QMessageBox::warning(this, tr("Not a File"),
			    tr("'%1' isn't a file.")
			        .arg(QDir::toNativeSeparators(fileName)));
			m_ui->dbFilePath->setText(QString());
			return;
		}

		if (Q_UNLIKELY(!fi.isReadable())) {
			QMessageBox::warning(this, tr("Not Readable"),
			    tr("File '%1' cannot be read.")
			        .arg(QDir::toNativeSeparators(fileName)));
			m_ui->dbFilePath->setText(QString());
			return;
		}
	}

	{
		CorruptMessageDb cmdb("CORRUPT_MESSAGE_DATABASE");
		if (Q_UNLIKELY(!cmdb.openDb(fileName))) {
			QMessageBox::warning(this, tr("Cannot Open Database"),
			    tr("File '%1' cannot be opened as a database.")
			        .arg(QDir::toNativeSeparators(fileName)));
			m_ui->dbFilePath->setText(QString());
			return;
		}

		if (Q_UNLIKELY(!cmdb.containsKnownTables())) {
			QMessageBox::warning(this, tr("Invalid Database Content"),
			    tr("File '%1' is either not a message database or doesn't contain a database at all.")
			        .arg(QDir::toNativeSeparators(fileName)));
			m_ui->dbFilePath->setText(QString());
			return;
		}
	}

	m_ui->dbFilePath->setText(QDir::toNativeSeparators(fileName));
}

void MWDbRepair::topMenuConnectActions(void) const
{
	/* File menu. */
	connect(m_ui->actionLoadDatabaseFile, SIGNAL(triggered()),
	    this, SLOT(loadMessageDbFile()));
	/* Separator. */
	/* actionQuit -- connected in ui file. */
}
