/*
 * 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 <algorithm> /* ::std::sort */
#include <cinttypes> /* PRId64 */
#include <QDir>
#include <QMenu>
#include <QMessageBox>
#include <QFileDialog>
#include <QPropertyAnimation>
#include <QSet>

#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/datovka_shared/crypto/crypto_wrapped.h" /* TstEntries::latestTstSigningCertTimes */
#include "src/datovka_shared/io/filesystem.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/settings/prefs.h"
#include "src/datovka_shared/utility/strings.h" /* Utility::generateRandomString */
#include "src/datovka_shared/worker/pool.h"
#include "src/dimensions/dimensions.h"
#include "src/gui/datovka.h"
#include "src/gui/dlg_msg_box_detail.h"
#include "src/gui/dlg_timestamp_renewal.h"
#include "src/gui/dlg_view_zfo.h"
#include "src/gui/message_operations.h" /* MsgOrigin */
#include "src/global.h"
#include "src/identifiers/composite.h" /* AcntIdToMsgIdListHash */
#include "src/io/isds_sessions.h"
#include "src/io/message_db_set_container.h"
#include "src/io/message_db_set.h"
#include "src/io/message_db.h"
#include "src/isds/message_functions.h" /* Isds::guessFileRawType */
#include "src/model_interaction/account_interaction.h"
#include "src/settings/accounts.h"
#include "src/settings/prefs_specific.h"
#include "src/views/on_widget_hidden_filter.h"
#include "src/views/table_home_end_filter.h"
#include "src/views/table_key_press_filter.h"
#include "src/views/table_space_selection_filter.h"
#include "src/views/table_tab_ignore_filter.h"
#include "src/worker/message_emitter.h"
#include "src/worker/task_archive_document.h"
#include "ui_dlg_timestamp_renewal.h"

#define TAB_TRACKED 0
#define TAB_ALL_STORED 1
#define TAB_EXTERNAL 2

#define PERIODIC_TIME_UPDATE_SECS 7200 /* 2 hours */

#define MSG_PROGRESS_COL MsgTstListingModel::COL_ERROR
#define ACNT_PROGRESS_COL BackupSelectionModel::COL_BOX_ID

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

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

static const QString dlgName("timestamp_renewal");
static const QString trackedTstTableName("tracked_timestamp_list");
static const QString allStoredAccountTableName("all_stored_account_list");
static const QString allStoredSplitterName("all_stored_splitter");
static const QString allStoredTstTableName("all_stored_timestamp_list");

ScanAccountsThread::ScanAccountsThread(volatile bool &stop)
    : QThread(),
    m_scannedAcntIdDbList(),
    m_stop(stop)
{
}

bool ScanAccountsThread::setScannedAcntIdDbList(
    const QList<AcntIdDb> &acntIdDbList)
{
	if (Q_UNLIKELY(isRunning())) {
		return false;
	}

	m_scannedAcntIdDbList = acntIdDbList;
	return true;
}

void ScanAccountsThread::run(void)
{
	Q_EMIT started();

	bool interrupted = false;
	for (const AcntIdDb &acntIdDb : m_scannedAcntIdDbList) {
		if (GlobInstcs::req_halt || m_stop) {
			interrupted = true;
			break;
		}

		MessageDbSet *dbSet = acntIdDb.messageDbSet();
		if (Q_UNLIKELY(Q_NULLPTR == dbSet)) {
			Q_ASSERT(0);
			break;
		}

		QSet<MsgId> msgIdSet = dbSet->getAllMsgIds();

#define PROGRESS_EMIT_GRANULARITY 10
#define GATHER_CLUSTER 10

		QList<TstEntry> tstEntries;
		int msgNum = -1;
		const int msgTotal = msgIdSet.size();
		for (const MsgId &msgId : msgIdSet) {
			++msgNum; /* Start at 0. */
			/* Emit progress at start and once a while. */
			if ((msgNum == 1) || ((msgNum % PROGRESS_EMIT_GRANULARITY) == 0)) {
				Q_EMIT progress(acntIdDb, msgNum, msgTotal);
			}

			if (GlobInstcs::req_halt || m_stop) {
				interrupted = true;
				break;
			}

			MessageDb *messageDb = dbSet->constAccessMessageDb(msgId.deliveryTime());
			if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
				logErrorNL("Couldn't access message database for username '%s' and message '%" PRId64 "'.",
				    acntIdDb.username().toUtf8().constData(),
				    UGLY_QINT64_CAST msgId.dmId());
				continue;
			}

			QByteArray msgData = messageDb->getCompleteMessageRaw(msgId.dmId());
			if (Q_UNLIKELY(msgData.isEmpty())) {
				logInfoNL("Couldn't access data for message '%" PRId64 "'.",
				    UGLY_QINT64_CAST msgId.dmId());
				continue;
			}

			QDateTime expir;
			{
				QDateTime incep;
				if (Q_UNLIKELY(!TstEntries::latestTstSigningCertTimes(
				        msgData, incep, expir))) {
					logErrorNL("Couldn't read timestamp expiration data for message '%" PRId64 "'.",
					    UGLY_QINT64_CAST msgId.dmId());
					continue;
				}
			}

			/* Gather entries. */
			tstEntries.append(TstEntry(
			    Json::MsgId2(acntIdDb.testing() ? Isds::Type::BOOL_TRUE : Isds::Type::BOOL_FALSE,
			        msgId.dmId(), msgId.deliveryTime()),
			    acntIdDb.username(), expir));

			/* Emit gathered once a while. */
			if ((tstEntries.size() % GATHER_CLUSTER) == 0) {
				Q_EMIT processed(acntIdDb, tstEntries);
				tstEntries.clear(); /* Clear when emitted. */
			}

		}
		Q_EMIT progress(acntIdDb, msgTotal, msgTotal);
		/* Emit gathered if some left. */
		if (tstEntries.size() > 0) {
			Q_EMIT processed(acntIdDb, tstEntries);
			tstEntries.clear(); /* Clear when emitted. */
		}
	}

#undef PROGRESS_EMIT_GRANULARITY
#undef GATHER_CLUSTER

	Q_EMIT stopped(interrupted);

	logDebugLv0NL("Timestamp expiration scan finished in thread '%p'.",
	    (void *) QThread::currentThreadId());
}

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

/*!
 * @brief Query and organise message entries according to account and message identifier.
 *
 * @param[in] tstEntries List if time-stamp entries.
 * @return Message entries ashed against account and message identifier.
 */
static
QHash<AcntId, QHash<MsgId, MessageDb::SoughtMsg> > gatherMessageEntries(
   const QList<TstEntry> &tstEntries)
{
	/* Sort message identifiers according to user names. */
	AcntIdToMsgIdListHash acntToMsgIds;
	for (const TstEntry &tstEntry : tstEntries) {
		const AcntId acntId(tstEntry.username, (tstEntry.msgId.testEnv() == Isds::Type::BOOL_TRUE));
		if (Q_UNLIKELY(!acntId.isValid())) {
			continue;
		}
		acntToMsgIds[acntId].append(
		    MsgId(tstEntry.msgId.dmId(),
		        tstEntry.msgId.deliveryTime()));
	}

	/* Get message entries according to user name and message id. */
	QHash<AcntId, QHash<MsgId, MessageDb::SoughtMsg> > msgEntries;
	for (const AcntId &acntId : acntToMsgIds.keys()) {
		enum AccountInteraction::AccessStatus status = AccountInteraction::AS_OK;
		QString dbDir;
		QString namesStr;
		MessageDbSet *dbSet = AccountInteraction::accessDbSet(
		    acntId, status, dbDir, namesStr);

		QHash<MsgId, MessageDb::SoughtMsg> foundEntries;

		if (Q_UNLIKELY(Q_NULLPTR == dbSet)) {
			continue;
		}
		if (dbSet->msgsGetMsgDataFromIds(
		        acntToMsgIds.value(acntId),
		        foundEntries)) {
			msgEntries[acntId] = macroStdMove(foundEntries);
		}
	}

	return msgEntries;
}

DlgTimestampRenewal::DlgTimestampRenewal(const QList<AcntIdDb> &acntIdDbList,
    TimestampDb *timestampDb,
    DbContainer *msgDbs, IsdsSessions *sessions, MainWindow *mw,
    QWidget *parent, Qt::WindowFlags flags)
    : QDialog(parent, flags),
    m_ui(new (::std::nothrow) Ui::DlgTimestampRenewal),
    m_trackedModelRowSelection(),
    m_allStoredModelRowSelection(),
    m_timestampDb(timestampDb),
    m_msgDbs(msgDbs),
    m_sessions(sessions),
    m_mw(mw),
    m_updateTimer(this),
    m_trackedSortProxyModel(this),
    m_trackedProgressProxyModel(this),
    m_progressDelegate(this),
    m_trackedTimestampTableModel(this),
    m_dfltFilerLineStyleSheet(SortFilterProxyModel::invalidFilterEditStyle),
    m_accountProgressProxyModel(this),
    m_accountSelectionModel(this),
    m_allStoredSortProxyModel(this),
    m_allStoredTimeProxyModel(this),
    m_allStoredProgressProxyModel(this),
    m_allStoredTimestampTableModel(this),
    m_trackedTransactionMap(),
    m_externalTransaction(),
    m_tasks(),
    m_externalTask(Q_NULLPTR),
    m_breakScanLoop(false),
    m_scanAccountsThread(m_breakScanLoop),
    m_speeds(),
    m_acntIdDbList(acntIdDbList),
    m_dfltSize()
{
	m_ui->setupUi(this);
	/* Tab order is defined in UI file. */

	m_ui->hSplitterAllStored->setChildrenCollapsible(false);

	m_dfltSize = this->size();
	{
		const QSize newSize = Dimensions::dialogueSize(this,
		    PrefsSpecific::dlgSize(*GlobInstcs::prefsPtr, dlgName),
		    m_dfltSize);
		if (newSize.isValid()) {
			this->resize(newSize);
		}
	}

	initTimestampWindow();

	{
		QStringList autoSelectedList;

		initialiseAccountModel(m_accountSelectionModel, m_acntIdDbList,
		    autoSelectedList);
	}

	/* Tracked messages view. */
	{
		TableKeyPressFilter *filter =
		    new (::std::nothrow) TableKeyPressFilter(m_ui->trackedMessagesView);
		if (filter != Q_NULLPTR) {
			filter->registerAction(Qt::CTRL | Qt::Key_F,
			    &showTrackedFilterLineViaFilter, this, true);
			m_ui->trackedMessagesView->installEventFilter(filter);
		}
	}

	/* Filter tracked line. */
	/* Clear tracked message filter line when line edit is hidden. */
	m_ui->trackedMessagesFilterHorizontalLayout->installEventFilter(
	    new (::std::nothrow) OnWidgetHiddenFilter(
	        &clearFilterLineViaFilter, m_ui->trackedMessagesFilterLine,
	        m_ui->trackedMessagesFilterLine));
	/* Hide tracked message filter line when escape pressed in line edit. */
	{
		TableKeyPressFilter *filter =
		    new (::std::nothrow) TableKeyPressFilter(m_ui->trackedMessagesFilterLine);
		if (Q_NULLPTR != filter) {
			filter->registerAction(Qt::Key_Escape,
			    &hideTrackedFilterLineViaFilter, this, true);
			m_ui->trackedMessagesFilterLine->installEventFilter(filter);
		}
	}

	/* Load tracked timestamps. */
	if ((Q_NULLPTR != m_timestampDb) && (Q_NULLPTR != m_msgDbs)) {
		QList<TstEntry> tstEntries;
		m_timestampDb->getMsgTimestampListing(tstEntries,
		    QDateTime(), QDateTime());

		QHash<AcntId, QHash<MsgId, MessageDb::SoughtMsg> > msgEntries =
		    gatherMessageEntries(tstEntries);

		m_trackedTimestampTableModel.appendData(tstEntries, msgEntries);
	}

	/* All messages view. */
	{
		TableKeyPressFilter *filter =
		    new (::std::nothrow) TableKeyPressFilter(m_ui->allMessagesView);
		if (filter != Q_NULLPTR) {
			filter->registerAction(Qt::CTRL | Qt::Key_F,
			    &showAllFilterLineViaFilter, this, true);
			m_ui->allMessagesView->installEventFilter(filter);
		}
	}

	/* Filter all stored line. */
	/* Clear all stored message filter line when line edit is hidden. */
	m_ui->allMessagesFilterHorizontalLayout->installEventFilter(
	    new (::std::nothrow) OnWidgetHiddenFilter(
	        &clearFilterLineViaFilter, m_ui->allMessagesFilterLine,
	        m_ui->allMessagesFilterLine));
	/* Hide all message filter line when escape pressed in line edit. */
	{
		TableKeyPressFilter *filter =
		    new (::std::nothrow) TableKeyPressFilter(m_ui->allMessagesFilterLine);
		if (Q_NULLPTR != filter) {
			filter->registerAction(Qt::Key_Escape,
			    &hideAllFilterLineViaFilter, this, true);
			m_ui->allMessagesFilterLine->installEventFilter(filter);
		}
	}

	{
		/* Load current notification value. */
		qint64 val = 0;
		if (GlobInstcs::prefsPtr->intVal("crypto.message.timestamp.expiration.notify_ahead.days", val)) {
			val = MsgTstListingModel::normaliseRestampingNeeded(val);

			m_trackedTimestampTableModel.setRestampingNeededDays(val);
			m_allStoredTimestampTableModel.setRestampingNeededDays(val);
		}
	}

	/* Periodically update time in components. */
	connect(&m_updateTimer, SIGNAL(timeout()),
	    &m_trackedTimestampTableModel, SLOT(updateCurrentTime()));
	connect(&m_updateTimer, SIGNAL(timeout()),
	    &m_allStoredTimestampTableModel, SLOT(updateCurrentTime()));
	connect(&m_updateTimer, &QTimer::timeout,
	    [=]() { this->applyRestampableFiltering(m_ui->onlyRestampableCheckBox->checkState()); });
	m_updateTimer.start(PERIODIC_TIME_UPDATE_SECS * 1000);
}

DlgTimestampRenewal::~DlgTimestampRenewal(void)
{
	PrefsSpecific::setDlgSize(*GlobInstcs::prefsPtr,
	    dlgName, this->size(), m_dfltSize);

	PrefsSpecific::setDlgTblRelColWidths(*GlobInstcs::prefsPtr,
	    dlgName, trackedTstTableName,
	    Dimensions::relativeTableColumnWidths(m_ui->trackedMessagesView));
	PrefsSpecific::setDlgTblColSortOrder(*GlobInstcs::prefsPtr,
	    dlgName, trackedTstTableName,
	    Dimensions::tableColumnSortOrder(m_ui->trackedMessagesView));

	PrefsSpecific::setDlgTblRelColWidths(*GlobInstcs::prefsPtr,
	    dlgName, allStoredAccountTableName,
	    Dimensions::relativeTableColumnWidths(m_ui->accountView));
	/* m_ui->accountView does not allow ordering. */

	PrefsSpecific::setDlgSplitterRelSizes(*GlobInstcs::prefsPtr,
	   dlgName, allStoredSplitterName,
	   Dimensions::relativeSplitterSizes(m_ui->hSplitterAllStored));

	PrefsSpecific::setDlgTblRelColWidths(*GlobInstcs::prefsPtr,
	    dlgName, allStoredTstTableName,
	    Dimensions::relativeTableColumnWidths(m_ui->allMessagesView));
	PrefsSpecific::setDlgTblColSortOrder(*GlobInstcs::prefsPtr,
	    dlgName, allStoredTstTableName,
	    Dimensions::tableColumnSortOrder(m_ui->allMessagesView));

	m_breakScanLoop = true;
	m_scanAccountsThread.wait();
	delete m_ui;
}

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

	/*
	 * https://stackoverflow.com/questions/4963220/how-to-preview-sizes-of-widgets-in-layout-before-a-show
	 *
	 * Cycle through all tabs before setting column widths.
	 */

	const int preset = m_ui->tabWidget->currentIndex();

	/* tracked */
	m_ui->tabWidget->setCurrentIndex(TAB_TRACKED);

	Dimensions::setRelativeTableColumnWidths(m_ui->trackedMessagesView,
	    PrefsSpecific::dlgTblRelColWidths(*GlobInstcs::prefsPtr,
	    dlgName, trackedTstTableName));
	Dimensions::setTableColumnSortOrder(m_ui->trackedMessagesView,
	    PrefsSpecific::dlgTblColSortOrder(*GlobInstcs::prefsPtr,
	    dlgName, trackedTstTableName));

	/* all stored */
	m_ui->tabWidget->setCurrentIndex(TAB_ALL_STORED);

	Dimensions::setRelativeTableColumnWidths(m_ui->accountView,
	    PrefsSpecific::dlgTblRelColWidths(*GlobInstcs::prefsPtr,
	    dlgName, allStoredAccountTableName));
	/* m_ui->accountView does not allow ordering. */

	Dimensions::setRelativeSplitterSizes(m_ui->hSplitterAllStored,
	    PrefsSpecific::dlgSplitterRelSizes(*GlobInstcs::prefsPtr,
	    dlgName, allStoredSplitterName));

	Dimensions::setRelativeTableColumnWidths(m_ui->allMessagesView,
	    PrefsSpecific::dlgTblRelColWidths(*GlobInstcs::prefsPtr,
	    dlgName, allStoredTstTableName));
	Dimensions::setTableColumnSortOrder(m_ui->allMessagesView,
	    PrefsSpecific::dlgTblColSortOrder(*GlobInstcs::prefsPtr,
	    dlgName, allStoredTstTableName));

	m_ui->tabWidget->setCurrentIndex(preset);
}

/*!
 * @brief Convert enum MessageDb::MessageType to enum MessageDirection.
 *
 * @param[in]  type Message type.
 * @param[out] ok Set to true on success.
 * @return Message direction.
 */
static
enum MessageDirection messageType2messageDirection(
    enum MessageDb::MessageType type, bool *ok = Q_NULLPTR)
{
	enum MessageDirection direct = MSG_ALL;

	switch (type) {
	case MessageDb::TYPE_RECEIVED:
		direct = MSG_RECEIVED;
		break;
	case MessageDb::TYPE_SENT:
		direct = MSG_SENT;
		break;
	default:
		Q_ASSERT(0);
		if (ok != Q_NULLPTR) {
			*ok = false;
		}
		return direct;
		break;
	}

	if (ok != Q_NULLPTR) {
		*ok = true;
	}
	return direct;
}

/*!
 * @brief Construct exhaustive message selection list.
 *
 * @brief Attempts to connect to ISDS with respective accounts.
 *
 * @param[in] modelRowSelection Message selection status.
 * @param[in] model Table model.
 * @param[in,out] sessions Session container.
 * @param[in] mw Main application window.
 * @return Origin list of selected messages.
 */
static
QList<MsgOrigin> messageOriginList(const QList<int> &modelRowSelection,
    const MsgTstListingModel &model, IsdsSessions *sessions, MainWindow *mw)
{
	QList<MsgOrigin> originList;

	QList<int> orderedRowSelection;

	/* Sort selected rows according to expiration time, oldest fist. */
	{
		/*
		 * Maps expiration time to wows.
		 * There may be multiple rows with same expiration times.
		 */
		QMap<QDateTime, QSet<int> > timeToRow;

		/* Map expiration times to rows. */
		for (int row : modelRowSelection) {
			const TstEntry &tstEntry = model.tstEntry(row);
			timeToRow[tstEntry.tstExpiration].insert(row);
		}

		QList<QDateTime> keys = timeToRow.keys();
		/* Sort expiration times. */
		::std::sort(keys.begin(), keys.end());

		/* Gather rows, older (lower) expiration times first. */
		for (const QDateTime &key : keys) {
			/* Row order with equal expiration time is unspecified. */
			for (int row : timeToRow[key]) {
				orderedRowSelection.append(row);
			}
		}
	}

	for (int row : orderedRowSelection) {
		if (Q_UNLIKELY(row < 0)) {
			Q_ASSERT(0);
			continue;
		}
		const TstEntry &tstEntry = model.tstEntry(row);
		const MessageDb::SoughtMsg &msgEntry = model.msgEntry(row);

		const AcntId acntId(tstEntry.username, (tstEntry.msgId.testEnv() == Isds::Type::BOOL_TRUE));

		enum AccountInteraction::AccessStatus status = AccountInteraction::AS_OK;
		QString dbDir;
		QString namesStr;
		MessageDbSet *dbSet = AccountInteraction::accessDbSet(
		    acntId, status, dbDir, namesStr);

		AcntIdDb acntIdDb(acntId, dbSet);
		if ((Q_NULLPTR != sessions) && (Q_NULLPTR != mw)) {
			if (!sessions->isConnectedToIsds(acntIdDb.username())) {
				if (Q_UNLIKELY(!mw->connectToIsds(acntIdDb))) {
					/* Ignore inactive accounts. */
					continue;
				}
			}
		}

		originList.append(MsgOrigin(acntIdDb, msgEntry.mId,
		    msgEntry.isVodz, messageType2messageDirection(msgEntry.type),
		    msgEntry.isDownloaded));
	}

	return originList;
}

void DlgTimestampRenewal::done(int resultCode)
{
	/* Don't ask if global halt requested. */
	if ((!GlobInstcs::req_halt)
	    && ((!m_tasks.empty()) || (Q_NULLPTR != m_externalTask))) {

		int ret = DlgMsgBoxDetail::message(this, QMessageBox::Question,
		    tr("Close Time Stamp Renewal Dialogue?"),
		    tr("Do you want to close this dialogue?"),
		    tr("Closing this dialogue will abort all unfinished actions."), QString(),
		    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
		if (ret == QMessageBox::No) {
			/* Don't close the dialogue. */
			return;
		}
	}

	/* Ask all pending tasks to quit. */
	for (TaskArchiveIsdsDocument *task : m_tasks) {
		task->requestQuit();
	}
	if (Q_NULLPTR != m_externalTask) {
		m_externalTask->requestQuit();
	}

	/*
	 * Wait for tasks to finish and collect them if to global shutdown
	 * required.
	 *
	 * TODO -- A better way to dispose discarded tasks must be implemented.
	 *
	 * The use of QCoreApplication::processEvents() is discouraged by Qt
	 * documentation.
	 *
	 * Extend worker/pool interface to accommodate more generic approach.
	 * All tasks should have a transaction/stak identifier.
	 * All tasks should have a requestQuit() method.
	 * The pool could have a storage for finished tasks.
	 * Finished tasks should be acquired via a poll interface rather than
	 * remembering their pointers in the caller code.
	 * Uncollected tasks should be deleted after a period of time?
	 */
	if (!GlobInstcs::req_halt) {
		while ((!m_tasks.empty()) || (Q_NULLPTR != m_externalTask)) {
			QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
		}
	}

	/* Close the dialogue. */
	QDialog::done(resultCode);
}

void DlgTimestampRenewal::renewSelectedTrackedTimestamps(void)
{
	renewSelectedTimestamps(m_trackedModelRowSelection,
	    m_trackedTimestampTableModel);
}

void DlgTimestampRenewal::renewSelectedAllStoredTimestamps(void)
{
	renewSelectedTimestamps(m_allStoredModelRowSelection,
	    m_allStoredTimestampTableModel);
}

/*!
 * @brief Translate source indexes through a list of proxy models.
 *
 * @note Every index is translated by mapToSource() of every proxy model on the
 *     order as it is defined by the list.
 *
 * @param[in] srcIndexList Index list as obtained from a view's selection model.
 * @param[in] proxyModelCascade List of proxy models, first is the most outer one.
 * @return List of translated indexes.
 */
static
QModelIndexList transitiveMapToSource(const QModelIndexList &srcIndexList,
    const QList<const QAbstractProxyModel *> &proxyModelCascade)
{
	QModelIndexList translatedIndexes;

	for (QModelIndex index : srcIndexList) {
		for (const QAbstractProxyModel *proxyModel : proxyModelCascade) {
			if (Q_UNLIKELY(Q_NULLPTR == proxyModel)) {
				Q_ASSERT(0);
				continue;
			}
			index = proxyModel->mapToSource(index);
		}
		translatedIndexes.append(index);
	}

	return translatedIndexes;
}

/*!
 * @brief Translate indexes of all rows in given column through supplied
 *     proxy models.
 *
 * @param[in] view View to take all the content from.
 * @param[in] column Number of the column.
 * @param[in] proxyModelCascade List of proxy models, first is the most outer one.
 * @return List or rows to the underlying model.
 */
static
QList<int> listedSrcRowNums(const QAbstractItemView &view, int column,
     const QList<const QAbstractProxyModel *> &proxyModelCascade)
{
	QList<int> rowList;

	QAbstractItemModel *model = view.model();
	if (Q_UNLIKELY(Q_NULLPTR == model)) {
		Q_ASSERT(0);
		return rowList;
	}

	/* Get all indexes. */
	QModelIndexList idxs;
	for (int row = 0; row < model->rowCount(); ++row) {
		idxs.append(model->index(row, column));
	}

	/* Translate to underlying model. */
	idxs = transitiveMapToSource(idxs, proxyModelCascade);
	for (const QModelIndex &idx : idxs) {
		rowList.append(idx.row());
	}

	return rowList;
}

void DlgTimestampRenewal::renewListedAllStoredTimestamps(void)
{
	const QList<int> listedAllStoredRows = listedSrcRowNums(
	    *(m_ui->allMessagesView), MsgTstListingModel::COL_ACCOUNT_ID,
	    {&m_allStoredSortProxyModel, &m_allStoredTimeProxyModel, &m_allStoredProgressProxyModel});

	renewSelectedTimestamps(m_allStoredModelRowSelection,
	    m_allStoredTimestampTableModel);
}

/*!
 * @brief Translate indexes of selected rows in given column through supplied
 *     proxy models.
 *
 * @param[in] view View to take the selection from.
 * @param[in] column Number of the column.
 * @param[in] proxyModelCascade List of proxy models, first is the most outer one.
 * @return List or rows to the underlying model.
 */
static
QList<int> selectedSrcRowNums(const QAbstractItemView &view, int column,
     const QList<const QAbstractProxyModel *> &proxyModelCascade)
{
	QList<int> rowList;

	QModelIndexList selIdxs = view.selectionModel()->selectedRows(column);

	/* Translate to underlying model. */
	selIdxs = transitiveMapToSource(selIdxs, proxyModelCascade);
	for (const QModelIndex &idx : selIdxs) {
		rowList.append(idx.row());
	}

	return rowList;
}

void DlgTimestampRenewal::updateTrackedMessageSelection(
    const QItemSelection &selected, const QItemSelection &deselected)
{
	Q_UNUSED(selected);
	Q_UNUSED(deselected);

	m_trackedModelRowSelection = selectedSrcRowNums(
	    *(m_ui->trackedMessagesView), MsgTstListingModel::COL_ACCOUNT_ID,
	    {&m_trackedSortProxyModel, &m_trackedProgressProxyModel});

	updateActionActivation();
}

void DlgTimestampRenewal::updateAllStoredMessageSelection(
    const QItemSelection &selected, const QItemSelection &deselected)
{
	Q_UNUSED(selected);
	Q_UNUSED(deselected);

	m_allStoredModelRowSelection = selectedSrcRowNums(
	    *(m_ui->allMessagesView), MsgTstListingModel::COL_ACCOUNT_ID,
	    {&m_allStoredSortProxyModel, &m_allStoredTimeProxyModel, &m_allStoredProgressProxyModel});

	updateActionActivation();
}

void DlgTimestampRenewal::watchAllStoredMessageList(void)
{
	updateActionActivation();
}

/*!
 * @brief Enable/disable timestamp tracking.
 *
 * @note See also MainWindow::toggleTimestampTracking().
 *
 * @param[in] timestampDb Timestamp database.
 * @param[in] timestampTableModel Timestamp table model.
 * @param[in] modelRowSelection List of selected rows.
 * @param[in] enableTracking Whether to enable or disable timestamp tracking.
 */
static
void toggleTimestampTracking(TimestampDb &timestampDb,
    const MsgTstListingModel &timestampTableModel,
    const QList<int> &modelRowSelection, bool enableTracking)
{
	if (Q_UNLIKELY(modelRowSelection.size() == 0)) {
		return;
	}

	if (enableTracking) {
		/* Only complete messages are listed. */

		QHash<AcntId, Json::MsgId2List> msgId2ListHash;
		TstValidityHash expirationValues;
		for (const int row : modelRowSelection) {
			const TstEntry &tstEntry =
			    timestampTableModel.tstEntry(row);
			if (Q_UNLIKELY(!tstEntry.tstExpiration.isValid())) {
				Q_ASSERT(0);
				continue;
			}
			const AcntId acntId(tstEntry.username,
			    tstEntry.msgId.testEnv() == Isds::Type::BOOL_TRUE);

			msgId2ListHash[acntId].append(tstEntry.msgId);

			/* Expirations are listed in the model. */
			expirationValues[
			    Json::MsgId1(tstEntry.msgId.testEnv(),
			        tstEntry.msgId.dmId())] =
			    TstValidity(true, tstEntry.tstExpiration);
		}

		for (const AcntId &acntId : msgId2ListHash.keys()) {
			timestampDb.insertIdentifications(msgId2ListHash[acntId], acntId);
		}
		if (!expirationValues.isEmpty()) {
			timestampDb.updateMsgTimestampData(expirationValues);
		}

	} else {
		Json::MsgId2List msgId2List;

		for (const int row : modelRowSelection) {
			const TstEntry &tstEntry =
			    timestampTableModel.tstEntry(row);

			msgId2List.append(tstEntry.msgId);
		}

		timestampDb.deleteIdentifications(msgId2List);
	}
}

void DlgTimestampRenewal::toggleTrackedTimestampTracking(bool enable)
{
	toggleTimestampTracking(*m_timestampDb, m_trackedTimestampTableModel,
	    m_trackedModelRowSelection, enable);
}

void DlgTimestampRenewal::toggleAllStoredTimestampTracking(bool enable)
{
	toggleTimestampTracking(*m_timestampDb, m_allStoredTimestampTableModel,
	    m_allStoredModelRowSelection, enable);
}

void DlgTimestampRenewal::viewTrackedMessagesListContextMenu(
    const QPoint &point)
{
	QModelIndex srcIdx = transitiveMapToSource(
	    {m_ui->trackedMessagesView->indexAt(point)},
	    {&m_trackedSortProxyModel, &m_trackedProgressProxyModel}).first();

	QMenu *menu = new (::std::nothrow) QMenu(m_ui->trackedMessagesView);
	if (Q_UNLIKELY(menu == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}

	if (srcIdx.isValid()) {
		menu->addAction(m_ui->actionTrackedSelectedRestamp);
		menu->addSeparator();

		/* All listed messages are tracked. */
		m_ui->actionTrackedTrackTimestampExpiration->setChecked(true);
		menu->addAction(m_ui->actionTrackedTrackTimestampExpiration);
	}

	menu->exec(QCursor::pos());
	menu->deleteLater();
}

void DlgTimestampRenewal::trackedMessageItemDoubleClicked(
    const QModelIndex &index)
{
	messageItemDoubleClicked(index,
	    {&m_trackedSortProxyModel, &m_trackedProgressProxyModel},
	    m_trackedTimestampTableModel);
}

void DlgTimestampRenewal::viewAllStoredMessagesListContextMenu(
    const QPoint &point)
{
	QModelIndex srcIdx = transitiveMapToSource(
	    {m_ui->allMessagesView->indexAt(point)},
	    {&m_allStoredSortProxyModel, &m_allStoredTimeProxyModel, &m_allStoredProgressProxyModel})
	        .first();

	QMenu *menu = new (::std::nothrow) QMenu(m_ui->allMessagesView);
	if (Q_UNLIKELY(menu == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}

	if (srcIdx.isValid()) {
		menu->addAction(m_ui->actionAllStoredSelectedRestamp);
		menu->addSeparator();

		bool completeSelectionTracked = false;
		if (m_allStoredModelRowSelection.size() > 0) {
			/* Organise selection according to accounts. */
			QHash<AcntId, Json::MsgId1List> msgId1ListHash;
			for (const int row : m_allStoredModelRowSelection) {
				const TstEntry &tstEntry = m_allStoredTimestampTableModel.tstEntry(row);
				const AcntId acntId(tstEntry.username, tstEntry.msgId.testEnv() == Isds::Type::BOOL_TRUE);

				msgId1ListHash[acntId].append(
				    Json::MsgId1(tstEntry.msgId.testEnv(), tstEntry.msgId.dmId()));
			}
			/* Sum number of tracked messages across selection/accounts. */
			qint64 trackedSum = 0;
			for (const AcntId &acntId : msgId1ListHash.keys()) {
				TstValidityHash tstValues;
				GlobInstcs::timestampDbPtr->getMsgTimestampData(msgId1ListHash[acntId],
				    tstValues);
				trackedSum += tstValues.size();
			}
			/* Leave unchecked if some values untracked. */
			completeSelectionTracked = (m_allStoredModelRowSelection.size() == trackedSum);
		}
		m_ui->actionAllStoredTrackTimestampExpiration->setChecked(completeSelectionTracked);

		menu->addAction(m_ui->actionAllStoredTrackTimestampExpiration);
	}

	menu->exec(QCursor::pos());
	menu->deleteLater();
}

void DlgTimestampRenewal::allStoredMessageItemDoubleClicked(
    const QModelIndex &index)
{
	messageItemDoubleClicked(index,
	    {&m_allStoredSortProxyModel, &m_allStoredTimeProxyModel, &m_allStoredProgressProxyModel},
	    m_allStoredTimestampTableModel);
}

/*!
 * @brief Filter listed messages.
 *
 * @param[in] dfltFilerLineStyleSheet Default filter line style sheet value.
 * @param[in] sortProxyModel Sort proxy model.
 * @param[in] filterLine Filter line widget.
 */
static
void filterMessages(const QString &text, QString &dfltFilerLineStyleSheet,
   SortFilterProxyModel &sortProxyModel, QLineEdit &filterLine)
{
	/* Store style at first invocation. */
	if (dfltFilerLineStyleSheet == SortFilterProxyModel::invalidFilterEditStyle) {
		dfltFilerLineStyleSheet = filterLine.styleSheet();
	}

	/* The model is always associated to the proxy model. */

	sortProxyModel.setSortRole(MsgTstListingModel::ROLE_PROXYSORT);

#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
	sortProxyModel.setFilterRegularExpression(
	    QRegularExpression(QRegularExpression::escape(text),
	        QRegularExpression::CaseInsensitiveOption));
#else /* < Qt-5.15 */
	sortProxyModel.setFilterRegExp(QRegExp(text,
	    Qt::CaseInsensitive, QRegExp::FixedString));
#endif /* >= Qt-5.15 */
	{
		QList<int> columnList;
		columnList.append(MsgTstListingModel::COL_ACCOUNT_ID);
		columnList.append(MsgTstListingModel::COL_MESSAGE_ID);
		columnList.append(MsgTstListingModel::COL_ANNOTATION);
		columnList.append(MsgTstListingModel::COL_EXPIRATION_TIME);
		columnList.append(MsgTstListingModel::COL_ERROR);
		sortProxyModel.setFilterKeyColumns(columnList);
	}

	/* Set filter field background colour. */
	if (text.isEmpty()) {
		filterLine.setStyleSheet(dfltFilerLineStyleSheet);
	} else if (sortProxyModel.rowCount() != 0) {
		filterLine.setStyleSheet(
		    SortFilterProxyModel::foundFilterEditStyle);
	} else {
		filterLine.setStyleSheet(
		    SortFilterProxyModel::notFoundFilterEditStyle);
	}
}

void DlgTimestampRenewal::filterTrackedMessages(const QString &text)
{
	debugSlotCall();

	filterMessages(text, m_dfltFilerLineStyleSheet, m_trackedSortProxyModel,
	    *(m_ui->trackedMessagesFilterLine));
}

void DlgTimestampRenewal::filterAllMessages(const QString &text)
{
	debugSlotCall();

	filterMessages(text, m_dfltFilerLineStyleSheet, m_allStoredSortProxyModel,
	    *(m_ui->allMessagesFilterLine));
}

#define ANIMATION_DURATION 250

/*!
 * @brief Show message filter line.
 *
 * @param[in,out] layoutWidget Widget holding the filter line to be shown.
 * @param[in,out] focusWidget Widget to set focus on after the animation finishes.
 */
static
void showFilterLine(QWidget *layoutWidget, QWidget *focusWidget)
{
	if (Q_UNLIKELY(Q_NULLPTR == layoutWidget)) {
		return;
	}
	if (Q_UNLIKELY(Q_NULLPTR == focusWidget)) {
		return;
	}

	if (!layoutWidget->isVisible()) {
		layoutWidget->setMaximumHeight(0);
		layoutWidget->setVisible(true);

		QPropertyAnimation *animation =
		    new (::std::nothrow) QPropertyAnimation(
		        layoutWidget, "maximumHeight");
		if (Q_NULLPTR != animation) {
			animation->setDuration(ANIMATION_DURATION);
			animation->setStartValue(0);
			animation->setEndValue(
			    layoutWidget->sizeHint().height());

			animation->start(QAbstractAnimation::DeleteWhenStopped);
		}
	}

	focusWidget->setFocus(Qt::OtherFocusReason);
}

/*!
 * @brief Hide message list filter.
 *
 * @param[in,out] layoutWidget Widget holding the filter line to be hidden.
 * @param[in,out] focusWidget Widget to set focus on after the animation finishes.
 */
static
void hideFilterLine(QWidget *layoutWidget, QWidget *focusWidget)
{
	if (Q_UNLIKELY(Q_NULLPTR == layoutWidget)) {
		return;
	}

	{
		QPropertyAnimation *animation =
		    new (::std::nothrow) QPropertyAnimation(
		        layoutWidget, "maximumHeight");
		if (Q_NULLPTR != animation) {
			animation->setDuration(ANIMATION_DURATION);
			animation->setStartValue(
			    layoutWidget->height());
			animation->setEndValue(0);

			/* Hide line after animation finishes. */
			DlgTimestampRenewal::connect(animation, SIGNAL(finished()),
			    layoutWidget, SLOT(hide()));

			animation->start(QAbstractAnimation::DeleteWhenStopped);
		}
	}
	layoutWidget->setMaximumHeight(QWIDGETSIZE_MAX);

	if (Q_NULLPTR != focusWidget) {
		focusWidget->setFocus(Qt::OtherFocusReason);
	}
}

#undef ANIMATION_DURATION

void DlgTimestampRenewal::hideTrackedMessagesFilter(void)
{
	hideFilterLine(m_ui->trackedMessagesFilterHorizontalLayout,
	    m_ui->trackedMessagesView);
}

void DlgTimestampRenewal::hideAllMessagesFilter(void)
{
	hideFilterLine(m_ui->allMessagesFilterHorizontalLayout,
	    m_ui->allMessagesView);
}

void DlgTimestampRenewal::watchIdentificationsInserted(
    const Json::MsgId2List &msgIds, const AcntId &acntId)
{
	QList<TstEntry> tstEntries;

	/* Sort message identifiers according to user names. */
	AcntIdToMsgIdListHash acntToMsgIds;
	for (const Json::MsgId2 &msgId : msgIds) {
		tstEntries.append(TstEntry(msgId, acntId.username(), QDateTime()));
		acntToMsgIds[acntId].append(
		    MsgId(msgId.dmId(), msgId.deliveryTime()));
	}

	/* Get message entries according to user name and message id. */
	QHash<AcntId, QHash<MsgId, MessageDb::SoughtMsg> > msgEntries;
	for (const AcntId &acntId : acntToMsgIds.keys()) {
		enum AccountInteraction::AccessStatus status = AccountInteraction::AS_OK;
		QString dbDir;
		QString namesStr;
		MessageDbSet *dbSet = AccountInteraction::accessDbSet(
		    acntId, status, dbDir, namesStr);

		QHash<MsgId, MessageDb::SoughtMsg> foundEntries;

		if (Q_UNLIKELY(Q_NULLPTR == dbSet)) {
			continue;
		}
		if (dbSet->msgsGetMsgDataFromIds(
		        acntToMsgIds.value(acntId),
		        foundEntries)) {
			msgEntries[acntId] = macroStdMove(foundEntries);
		}

	}

	m_trackedTimestampTableModel.appendData(tstEntries, msgEntries);
}

void DlgTimestampRenewal::watchIdentificationsDeleted(
    const Json::MsgId2List &msgIds)
{
	m_trackedTimestampTableModel.remove(msgIds);
}

void DlgTimestampRenewal::watchMsgTimestampDataUpdated(
    const TstValidityHash &values)
{
	m_trackedTimestampTableModel.updateExpiration(values);
}

void DlgTimestampRenewal::watchPrefsModification(int valueType,
    const QString &name, const QVariant &value)
{
	switch (valueType) {
	case PrefsDb::VAL_INTEGER:
		if (name == "crypto.message.timestamp.expiration.notify_ahead.days") {
			bool ok = false;
			qint64 val = value.toLongLong(&ok);
			if (Q_UNLIKELY(!ok)) {
				logErrorNL("%s", "Cannot convert value.");
				return;
			}
			val = MsgTstListingModel::normaliseRestampingNeeded(val);

			m_trackedTimestampTableModel.setRestampingNeededDays(val);
			m_allStoredTimestampTableModel.setRestampingNeededDays(val);
		}
		break;
	default:
		/* Do nothing. */
		break;
	}
}

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

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

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

	updateActionActivation();
}

void DlgTimestampRenewal::applyRestampableFiltering(int state)
{
	if (state == Qt::Unchecked) {
		/* Disable time filter. */
		m_allStoredTimeProxyModel.setFilterDateTimeRange(QDateTime(),
		    QDateTime());
	} else {
		const QDateTime currentTime = QDateTime::currentDateTime();
		m_allStoredTimeProxyModel.setFilterDateTimeRange(currentTime,
		    currentTime.addDays(RESTAMPING_INTERVAL_SUGGESTED));
	}
}

void DlgTimestampRenewal::modifiedAccountSelection(void)
{
	enum Qt::CheckState state = Qt::PartiallyChecked;

	if (((m_accountSelectionModel.rowCount() == 0) ||
	     (m_accountSelectionModel.numChecked() == m_accountSelectionModel.rowCount()))) {
		state = Qt::Checked;
	} else if ((m_accountSelectionModel.numChecked() == 0)) {
		state = Qt::Unchecked;
	}

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

	updateActionActivation();
}

/*!
 * @brief Find AcntIdDb matching the supplied \a acntId.
 *
 * @param[in] acntIdDbList List of AcntIdDb identifiers.
 * @param[in] acntId Sought AcntId identifier.
 * @return Valid AcntIdDb identifier if found, invalid if not found.
 */
static
AcntIdDb findAcntIdDbInList(const QList<AcntIdDb> &acntIdDbList, const AcntId &acntId)
{
	for (const AcntIdDb &acntIdDb : acntIdDbList) {
		if ((acntId.username() == acntIdDb.username())
		    && (acntId.testing() == acntIdDb.testing())) {
			return acntIdDb;
		}
	}

	return AcntIdDb();
}

void DlgTimestampRenewal::startAccountScan(void)
{
	/* Get list of selected accounts. */
	const QList<AcntId> selectedAcntIds =
	    m_accountSelectionModel.checkedAcntIds();
	if (Q_UNLIKELY(selectedAcntIds.isEmpty())) {
		return;
	}

	QList<AcntIdDb> selectedAcntIdDbs;
	for (const AcntId &acntId : selectedAcntIds) {
		AcntIdDb acntIdDb = findAcntIdDbInList( m_acntIdDbList, acntId);
		if (acntIdDb.isValid()) {
			selectedAcntIdDbs.append(acntIdDb);
		}
	}

	if (Q_UNLIKELY(!m_scanAccountsThread.setScannedAcntIdDbList(selectedAcntIdDbs))) {
		logErrorNL("%s",
		    "Cannot pass list of selected accounts into thread.");
		return;
	}

	m_allStoredTimestampTableModel.removeAllRows();
	/* Clear selection state as model has been reset. */
	m_allStoredModelRowSelection.clear();

	m_breakScanLoop = false;
	m_scanAccountsThread.start();

	updateActionActivation();
}

void DlgTimestampRenewal::interruptAccountScan(void)
{
	m_breakScanLoop = true;

	/* The method updateActionActivation() is called from watchScanStopped(). */
}

void DlgTimestampRenewal::watchScanStopped(bool interrupted)
{
	Q_UNUSED(interrupted);

	/* Wait for the thread to really finish. */
	m_scanAccountsThread.wait();
	m_breakScanLoop = false;

	updateActionActivation();
}

void DlgTimestampRenewal::watchScanProcessed(const AcntIdDb &acntIdDb,
    const QList<TstEntry> &tstEntries)
{
	Q_UNUSED(acntIdDb);

	QHash<AcntId, QHash<MsgId, MessageDb::SoughtMsg> > msgEntries =
	    gatherMessageEntries(tstEntries);

	m_allStoredTimestampTableModel.appendData(tstEntries, msgEntries);
}

void DlgTimestampRenewal::watchScanProgress(const AcntIdDb &acntIdDb, int num,
    int total)
{
	{
		int row = m_accountSelectionModel.rowOf(acntIdDb);
		if (row >= 0) {
			if (num < total) {
				m_accountProgressProxyModel.setProgress(row, ACNT_PROGRESS_COL,
				    0, total, num, QString());
			} else {
				m_accountProgressProxyModel.clearProgress(row, ACNT_PROGRESS_COL);
			}
		}
	}
}

/*!
 * @brief Get last directory where the external file was loaded from.
 */
static
QString loadArchivedZfoDir(const Prefs &prefs)
{
	QString val;
	prefs.strVal("global.path.load.archive_zfo", val);
	/* Has default value set. */
	return val;
}

/*!
 * @brief Store directory where the external file was loaded from.
 */
static
void setLoadArchivedZfoDir(Prefs &prefs, const QString &val)
{
	prefs.setStrVal("global.path.load.archive_zfo", val);
}

/*!
 * @brief Generate a target file name suggestion.
 *
 * @param[in] srcPath Source file path.
 * @return Altered file name in same location.
 */
static
QString generateTgtFilePath(const QString &srcPath)
{
	if (Q_UNLIKELY(srcPath.isEmpty())) {
		return QString();
	}

	const QFileInfo fi(srcPath);

	QString tgtPath = fi.absolutePath() + "/"
	    + fi.baseName() + "_" + DlgTimestampRenewal::tr("restamped")
	    + "." + fi.completeSuffix();

	return nonconflictingFileName(tgtPath);
}

void DlgTimestampRenewal::selectExternalSrcZfo(void)
{
	QString lastArchivePath = loadArchivedZfoDir(*GlobInstcs::prefsPtr);

	QFileDialog dialog(this, tr("Select ZFO File"));
	dialog.setDirectory(lastArchivePath);
	dialog.setNameFilter(tr("ZFO file (*.zfo)"));
	dialog.setFileMode(QFileDialog::ExistingFile);
	QStringList fileNames;

	if (dialog.exec()) {
		fileNames = dialog.selectedFiles();
		lastArchivePath = dialog.directory().absolutePath();
		setLoadArchivedZfoDir(*GlobInstcs::prefsPtr, lastArchivePath);
	}

	if (fileNames.size() > 0) {
		const QString &fileName = fileNames.at(0);
		{
			const QFileInfo fi(fileName);

			if ((!fi.isFile()) || (!fi.isReadable())) {
				QMessageBox::warning(this, tr("Not a Readable File"),
				    tr("The path '%1' doesn't point onto a readable file.")
				        .arg(QDir::toNativeSeparators(fileName)));
				return;
			}
		}

		if (Q_UNLIKELY(Isds::Type::RT_UNKNOWN == Isds::guessFileRawType(fileName))) {
			QMessageBox::warning(this, tr("No ZFO Content"),
			    tr("The file '%1' doesn't seem to contain a valid message or delivery info.")
			        .arg(QDir::toNativeSeparators(fileName)));
			return;
		}

		m_ui->srcZfoLineEdit->setText(
		    QDir::toNativeSeparators(fileName));

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

	updateActionActivation();
}

void DlgTimestampRenewal::viewExternalSrcZfo(void)
{
	if (Q_UNLIKELY(m_ui->srcZfoLineEdit->text().isEmpty())) {
		return;
	}

	DlgViewZfo::view(
	    QDir::fromNativeSeparators(m_ui->srcZfoLineEdit->text()), this);
}

void DlgTimestampRenewal::selectExternalTgtZfo(void)
{
	QString fileName = QFileDialog::getSaveFileName(this,
	    tr("Select ZFO File"),
	    QDir::fromNativeSeparators(m_ui->tgtZfoLineEdit->text()),
	    tr("ZFO file (*.zfo)"));

	m_ui->tgtZfoLineEdit->setText(QDir::toNativeSeparators(fileName));

	updateActionActivation();
}

void DlgTimestampRenewal::renewExternalTimestamps(void)
{
	const QString timeStr = QDateTime::currentDateTimeUtc().toString();

	const QString srcPath = QDir::fromNativeSeparators(m_ui->srcZfoLineEdit->text());
	const QString tgtPath = QDir::fromNativeSeparators(m_ui->tgtZfoLineEdit->text());

	const QFileInfo tgtFi(tgtPath);
	if (tgtFi.exists()) {
		if (!tgtFi.isFile()) {
			QMessageBox::warning(this, tr("Target not a File"),
			    tr("The target '%1' exists and is not a file.\n"
			        "Please choose a different target or delete the current one.")
			        .arg(m_ui->tgtZfoLineEdit->text()));
				return;
		} else {
			if (!tgtFi.isWritable()) {
				QMessageBox::warning(this, tr("Target not Writeable"),
				    tr("The target '%1' exists but it is not writeable.\n"
				        "Please choose a different target or ensure that it can be written to.")
				        .arg(m_ui->tgtZfoLineEdit->text()));
				return;
			}
			if (srcPath == tgtPath) {
				const int ret = QMessageBox::question(this,
				    tr("Source and Target are Equal"),
				    tr("The target '%1' corresponds to the source.\n"
				        "Do you want to overwrite the source file?")
				        .arg(m_ui->tgtZfoLineEdit->text()));
				if (QMessageBox::No == ret) {
					return;
				}
			}
		}
	}
	{
		const QFileInfo pathFi(tgtFi.absolutePath());
		if (!pathFi.exists()) {
			QMessageBox::warning(this, tr("Location Non-Existent"),
			    tr("The target location '%1' doesn't exist.\n"
			        "Please choose a different target or create the chosen one.")
			        .arg(QDir::toNativeSeparators(tgtFi.absolutePath())));
			return;
		} else {
			if (!pathFi.isWritable()) {
				QMessageBox::warning(this, tr("Location not Writeable"),
				    tr("The target location '%1' exists but it is not writeable.\n"
				        "Please choose a different location or ensure that it can be written to.")
				        .arg(QDir::toNativeSeparators(tgtFi.absolutePath())));
				return;
			}
		}
	}

	const AcntIdDb &acntIdDb =
	    m_acntIdDbList.at(m_ui->accountComboBox->currentIndex());

	if (!m_sessions->isConnectedToIsds(acntIdDb.username())) {
		if (Q_UNLIKELY(!m_mw->connectToIsds(acntIdDb))) {
			/* Ignore inactive accounts. */
			return;
		}
	}

	m_ui->restampSelectedTrackedPushButton->setEnabled(false);
	m_ui->actionTrackedSelectedRestamp->setEnabled(false);
	m_ui->restampSelectedAllStoredPushButton->setEnabled(false);
	m_ui->actionAllStoredSelectedRestamp->setEnabled(false);
	m_ui->restampListedAllStoredPushButton->setEnabled(false);
	m_ui->restampExternalPushButton->setEnabled(false);
	this->setCursor(Qt::BusyCursor);

	const QString transactionId = QString("%1_%2_%3_%4")
	    .arg(acntIdDb.username())
	    .arg(timeStr)
	    .arg("external")
	    .arg(Utility::generateRandomString(6));

	m_ui->singleProgressBar->setMinimum(0);
	m_ui->singleProgressBar->setMaximum(100);
	m_ui->singleProgressBar->setValue(0);
	m_ui->singleProgressBar->setEnabled(true);
	m_ui->singleProgressBar->setVisible(true);
	m_ui->singleResultLabel->setText(QString());

	TaskArchiveIsdsDocumentFile *task = new (::std::nothrow) TaskArchiveIsdsDocumentFile(
	    acntIdDb, transactionId,
	    srcPath, tgtPath);
	if (Q_UNLIKELY(task == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}

	m_externalTransaction = transactionId;
	m_externalTask = task;

	task->setAutoDelete(false);
	GlobInstcs::workPoolPtr->assignLo(task);
}

void DlgTimestampRenewal::watchUploadProgress(const AcntId &acntId,
    const QString &transactId, qint64 uploadTotal, qint64 uploadCurrent)
{
	Q_UNUSED(acntId);

	if (m_trackedTransactionMap.contains(transactId)) {

		if (Q_UNLIKELY(!m_speeds.contains(transactId))) {
			m_speeds[transactId] = NetworkSpeed::createCounter(uploadCurrent);
		} else {
			NetworkSpeed &netSpeed = m_speeds[transactId];
			netSpeed.updateCounter(uploadCurrent);
		}

		const NetworkSpeed &netSpeed = m_speeds[transactId];

		const QPair<AcntId, MsgId> pair = m_trackedTransactionMap.value(transactId);
		{
			int row = m_trackedTimestampTableModel.rowOf(pair.first, pair.second);
			if (row >= 0) {
				uploadTotal = 2 * uploadTotal;

				m_trackedProgressProxyModel.setProgress(row, MSG_PROGRESS_COL,
				    0, uploadTotal, uploadCurrent, netSpeed.uploadOverview());
			}
		}
		{
			int row = m_allStoredTimestampTableModel.rowOf(pair.first, pair.second);
			if (row >= 0) {
				uploadTotal = 2 * uploadTotal;

				m_allStoredProgressProxyModel.setProgress(row, MSG_PROGRESS_COL,
				    0, uploadTotal, uploadCurrent, netSpeed.uploadOverview());
			}
		}

	} else if (m_externalTransaction == transactId) {

		if (Q_UNLIKELY(!m_speeds.contains(transactId))) {
			m_speeds[transactId] = NetworkSpeed::createCounter(uploadCurrent);
		} else {
			NetworkSpeed &netSpeed = m_speeds[transactId];
			netSpeed.updateCounter(uploadCurrent);
		}

		const NetworkSpeed &netSpeed = m_speeds[transactId];

		uploadTotal = 2 * uploadTotal;

		m_ui->singleProgressBar->setMinimum(0);
		m_ui->singleProgressBar->setMaximum(uploadTotal);
		m_ui->singleProgressBar->setValue(uploadCurrent);
		/*
		 * Don't set speed in m_ui->singleProgressBar->setFormat()
		 * because when maximum us unknown the no text is displayed.
		 */
		m_ui->singleResultLabel->setText(netSpeed.uploadOverview());

	}
}

void DlgTimestampRenewal::watchUploadProgressFinished(const AcntId &acntId,
    const QString &transactId, int result, const QString &resultDesc,
    const QVariant &resultVal)
{
	Q_UNUSED(acntId);
	Q_UNUSED(result);
	Q_UNUSED(resultDesc);
	Q_UNUSED(resultVal);

	if (m_trackedTransactionMap.contains(transactId)) {

		m_speeds.remove(transactId);

		const QPair<AcntId, MsgId> pair = m_trackedTransactionMap.value(transactId);
		{
			int row = m_trackedTimestampTableModel.rowOf(pair.first, pair.second);
			if (row >= 0) {
				m_trackedProgressProxyModel.clearProgress(
				    row, MSG_PROGRESS_COL);
			}
		}
		{
			int row = m_allStoredTimestampTableModel.rowOf(pair.first, pair.second);
			if (row >= 0) {
				m_allStoredProgressProxyModel.clearProgress(
				    row, MSG_PROGRESS_COL);
			}
		}

	} else if (m_externalTransaction == transactId) {

		m_speeds.remove(transactId);

		m_ui->singleProgressBar->setMinimum(0);
		m_ui->singleProgressBar->setMaximum(0);
		m_ui->singleProgressBar->setValue(0);
		m_ui->singleResultLabel->setText(QString());

	}
}

void DlgTimestampRenewal::watchDownloadProgress(const AcntId &acntId,
    const QString &transactId, const MsgId &msgId,
    qint64 downloadTotal, qint64 downloadCurrent)
{
	if (m_trackedTransactionMap.contains(transactId)) {

		if (Q_UNLIKELY(!m_speeds.contains(transactId))) {
			m_speeds[transactId] = NetworkSpeed::createCounter(downloadCurrent);
		} else {
			NetworkSpeed &netSpeed = m_speeds[transactId];
			netSpeed.updateCounter(downloadCurrent);
		}

		const NetworkSpeed &netSpeed = m_speeds[transactId];

		if (downloadTotal > 0) {
			downloadCurrent = downloadCurrent + downloadTotal;
			downloadTotal = 2 * downloadTotal;
		}

		{
			int row = m_trackedTimestampTableModel.rowOf(acntId, msgId);
			if (row >= 0) {
				m_trackedProgressProxyModel.setProgress(row, MSG_PROGRESS_COL,
				    0, downloadTotal, downloadCurrent, netSpeed.downloadOverview());
			}
		}
		{
			int row = m_allStoredTimestampTableModel.rowOf(acntId, msgId);
			if (row >= 0) {
				m_allStoredProgressProxyModel.setProgress(row, MSG_PROGRESS_COL,
				    0, downloadTotal, downloadCurrent, netSpeed.downloadOverview());
			}
		}

	} else if (m_externalTransaction == transactId) {

		if (Q_UNLIKELY(!m_speeds.contains(transactId))) {
			m_speeds[transactId] = NetworkSpeed::createCounter(downloadCurrent);
		} else {
			NetworkSpeed &netSpeed = m_speeds[transactId];
			netSpeed.updateCounter(downloadCurrent);
		}

		const NetworkSpeed &netSpeed = m_speeds[transactId];

		if (downloadTotal > 0) {
			downloadCurrent = downloadCurrent + downloadTotal;
			downloadTotal = 2 * downloadTotal;
		}

		m_ui->singleProgressBar->setMinimum(0);
		m_ui->singleProgressBar->setMaximum(downloadTotal);
		m_ui->singleProgressBar->setValue(downloadCurrent);
		/*
		 * Don't set speed in m_ui->singleProgressBar->setFormat()
		 * because when maximum us unknown the no text is displayed.
		 */
		m_ui->singleResultLabel->setText(netSpeed.downloadOverview());
	}
}

void DlgTimestampRenewal::watchDownloadProgressFinished(const AcntId &acntId,
    const QString &transactId, const MsgId &msgId,
    int result, const QString &resultDesc)
{
	Q_UNUSED(result);

	if (m_trackedTransactionMap.contains(transactId)) {

		m_speeds.remove(transactId);

		{
			int row = m_trackedTimestampTableModel.rowOf(acntId, msgId);
			if (row >= 0) {
				m_trackedProgressProxyModel.clearProgress(
				    row, MSG_PROGRESS_COL);
			}
		}
		{
			int row = m_allStoredTimestampTableModel.rowOf(acntId, msgId);
			if (row >= 0) {
				m_allStoredProgressProxyModel.clearProgress(
				    row, MSG_PROGRESS_COL);
			}
		}

		m_trackedTimestampTableModel.updateError(acntId, msgId, resultDesc);
		m_allStoredTimestampTableModel.updateError(acntId, msgId, resultDesc);

		m_trackedTransactionMap.remove(transactId);

		TaskArchiveIsdsDocument *task = m_tasks.take(transactId);

		/* Update expiration time in database and model. */
		if ((Q_NULLPTR != task)
		        && (TaskArchiveIsdsDocument::AID_SUCCESS == task->m_result)
		        && (task->m_newExpirationTime.isValid())) {
			TstValidityHash expirationValues;
			expirationValues[Json::MsgId1(acntId.testing() ? Isds::Type::BOOL_TRUE : Isds::Type::BOOL_FALSE, msgId.dmId())] =
			    TstValidity(true, task->m_newExpirationTime);
			/*
			 * m_trackedTimestampTableModel is updated via m_timestampDb.
			 * m_allStoredTimestampTableModel is updated from here.
			 */
			m_timestampDb->updateMsgTimestampData(expirationValues);
			m_allStoredTimestampTableModel.updateExpiration(expirationValues);
		}

		delete task;

		if (m_tasks.isEmpty() && (Q_NULLPTR == m_externalTask)) {
			updateActionActivation();
			this->setCursor(Qt::ArrowCursor);
		}

	} else if (m_externalTransaction == transactId) {

		m_speeds.remove(transactId);

		m_ui->singleProgressBar->setMinimum(0);
		m_ui->singleProgressBar->setMaximum(0);
		m_ui->singleProgressBar->setValue(0);
		m_ui->singleProgressBar->resetFormat();
		m_ui->singleProgressBar->setEnabled(false);
		m_ui->singleProgressBar->setVisible(false);
		m_ui->singleResultLabel->setText(resultDesc);

		m_externalTransaction.clear();
		delete m_externalTask; m_externalTask = Q_NULLPTR;

		if (m_tasks.isEmpty() && (Q_NULLPTR == m_externalTask)) {
			updateActionActivation();
			this->setCursor(Qt::ArrowCursor);
		}

	}
}

void DlgTimestampRenewal::initTimestampWindow(void)
{
	/* tracked */
	{
		/* Set default line height for table views/widgets. */
		m_ui->trackedMessagesView->setNarrowedLineHeight();
		m_ui->trackedMessagesView->horizontalHeader()->setDefaultAlignment(
		    Qt::AlignVCenter | Qt::AlignLeft);

		m_ui->trackedMessagesView->setContextMenuPolicy(Qt::CustomContextMenu);
		connect(m_ui->trackedMessagesView, SIGNAL(customContextMenuRequested(QPoint)),
		    this, SLOT(viewTrackedMessagesListContextMenu(QPoint)));
		m_ui->trackedMessagesView->setSelectionMode(
		    QAbstractItemView::ExtendedSelection);
		m_ui->trackedMessagesView->setSelectionBehavior(
		    QAbstractItemView::SelectRows);

		m_trackedProgressProxyModel.setSourceModel(&m_trackedTimestampTableModel);

		m_trackedSortProxyModel.setSortRole(MsgTstListingModel::ROLE_PROXYSORT);
		m_trackedSortProxyModel.setSourceModel(&m_trackedProgressProxyModel);
		{
			QList<int> columnList;
			columnList.append(MsgTstListingModel::COL_ACCOUNT_ID);
			columnList.append(MsgTstListingModel::COL_MESSAGE_ID);
			columnList.append(MsgTstListingModel::COL_ANNOTATION);
			columnList.append(MsgTstListingModel::COL_EXPIRATION_TIME);
			columnList.append(MsgTstListingModel::COL_ERROR);
			m_trackedSortProxyModel.setFilterKeyColumns(columnList);
		}
		m_ui->trackedMessagesView->setModel(&m_trackedSortProxyModel);
		m_ui->trackedMessagesView->setItemDelegateForColumn(MSG_PROGRESS_COL, &m_progressDelegate);

		m_ui->trackedMessagesView->setSortingEnabled(true);
		m_ui->trackedMessagesView->sortByColumn(MsgTstListingModel::COL_ACCOUNT_ID,
		    Qt::AscendingOrder);

		connect(m_ui->trackedMessagesView, SIGNAL(doubleClicked(QModelIndex)),
		    this, SLOT(trackedMessageItemDoubleClicked(QModelIndex)));

		{
			TableKeyPressFilter *filter =
			    new (::std::nothrow) TableKeyPressFilter(m_ui->trackedMessagesView);
			if (Q_NULLPTR != filter) {
				filter->registerAction(Qt::Key_Return,
				    focusOnTrackedMessageViaFilter, this, true);
				filter->registerAction(Qt::Key_Enter, /* On keypad. */
				    focusOnTrackedMessageViaFilter, this, true);
				m_ui->trackedMessagesView->installEventFilter(filter);
			}
		}

		m_ui->trackedMessagesView->installEventFilter(
		    new (::std::nothrow) TableHomeEndFilter(m_ui->trackedMessagesView));
		m_ui->trackedMessagesView->installEventFilter(
		    new (::std::nothrow) TableTabIgnoreFilter(m_ui->trackedMessagesView));

		connect(m_ui->trackedMessagesView->selectionModel(),
		    SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
		    this,
		    SLOT(updateTrackedMessageSelection(QItemSelection, QItemSelection)));

		m_ui->trackedMessagesFilterHorizontalLayout->setVisible(false);
		connect(m_ui->trackedMessagesFilterLine, SIGNAL(textChanged(QString)),
		    this, SLOT(filterTrackedMessages(QString)));
		m_ui->closeTrackedFilterButton->setIcon(style()->standardIcon(QStyle::SP_DockWidgetCloseButton));
		connect(m_ui->closeTrackedFilterButton, SIGNAL(clicked()),
		    this, SLOT(hideTrackedMessagesFilter()));

		connect(m_ui->restampSelectedTrackedPushButton, SIGNAL(clicked()),
		    this, SLOT(renewSelectedTrackedTimestamps()));
	}

	/* all stored */
	{
		m_ui->allCheckBox->setCheckState(Qt::Unchecked);
		m_ui->allCheckBox->setTristate(false);

		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_accountProgressProxyModel.setSourceModel(&m_accountSelectionModel);

		m_ui->accountView->setModel(&m_accountProgressProxyModel);
		m_ui->accountView->setItemDelegateForColumn(ACNT_PROGRESS_COL, &m_progressDelegate);

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

		m_ui->allMessagesView->setNarrowedLineHeight();
		m_ui->allMessagesView->horizontalHeader()->setDefaultAlignment(
		    Qt::AlignVCenter | Qt::AlignLeft);

		m_ui->allMessagesView->setContextMenuPolicy(Qt::CustomContextMenu);
		connect(m_ui->allMessagesView, SIGNAL(customContextMenuRequested(QPoint)),
		    this, SLOT(viewAllStoredMessagesListContextMenu(QPoint)));
		m_ui->allMessagesView->setSelectionMode(
		    QAbstractItemView::ExtendedSelection);
		m_ui->allMessagesView->setSelectionBehavior(
		    QAbstractItemView::SelectRows);

		m_allStoredProgressProxyModel.setSourceModel(&m_allStoredTimestampTableModel);

		m_allStoredTimeProxyModel.setSourceModel(&m_allStoredProgressProxyModel);
		m_allStoredTimeProxyModel.setFilterRole(MsgTstListingModel::ROLE_PLAIN_DISPLAY);
		m_allStoredTimeProxyModel.setFilterKeyColumn(MsgTstListingModel::COL_EXPIRATION_TIME);

		m_allStoredSortProxyModel.setSortRole(MsgTstListingModel::ROLE_PROXYSORT);
		m_allStoredSortProxyModel.setSourceModel(&m_allStoredTimeProxyModel);
		{
			QList<int> columnList;
			columnList.append(MsgTstListingModel::COL_ACCOUNT_ID);
			columnList.append(MsgTstListingModel::COL_MESSAGE_ID);
			columnList.append(MsgTstListingModel::COL_ANNOTATION);
			columnList.append(MsgTstListingModel::COL_EXPIRATION_TIME);
			columnList.append(MsgTstListingModel::COL_ERROR);
			m_allStoredSortProxyModel.setFilterKeyColumns(columnList);
		}
		m_ui->allMessagesView->setModel(&m_allStoredSortProxyModel);
		m_ui->allMessagesView->setItemDelegateForColumn(MSG_PROGRESS_COL, &m_progressDelegate);

		m_ui->allMessagesView->setSortingEnabled(true);
		m_ui->allMessagesView->sortByColumn(MsgTstListingModel::COL_ACCOUNT_ID,
		    Qt::AscendingOrder);

		connect(m_ui->allMessagesView, SIGNAL(doubleClicked(QModelIndex)),
		    this, SLOT(allStoredMessageItemDoubleClicked(QModelIndex)));

		{
			TableKeyPressFilter *filter =
			    new (::std::nothrow) TableKeyPressFilter(m_ui->allMessagesView);
			if (Q_NULLPTR != filter) {
				filter->registerAction(Qt::Key_Return,
				    focusOnAllMessageViaFilter, this, true);
				filter->registerAction(Qt::Key_Enter, /* On keypad. */
				    focusOnAllMessageViaFilter, this, true);
				m_ui->allMessagesView->installEventFilter(filter);
			}
		}

		m_ui->allMessagesView->installEventFilter(
		    new (::std::nothrow) TableHomeEndFilter(m_ui->allMessagesView));
		m_ui->allMessagesView->installEventFilter(
		    new (::std::nothrow) TableTabIgnoreFilter(m_ui->allMessagesView));

		connect(m_ui->allMessagesView->selectionModel(),
		    SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
		    this,
		    SLOT(updateAllStoredMessageSelection(QItemSelection, QItemSelection)));
		connect(m_ui->allMessagesView->model(),
		    SIGNAL(rowsInserted(QModelIndex, int, int)),
		    this, SLOT(watchAllStoredMessageList()));
		connect(m_ui->allMessagesView->model(), SIGNAL(modelReset()),
		    this, SLOT(watchAllStoredMessageList()));
		connect(m_ui->allMessagesView->model(),
		    SIGNAL(rowsInserted(QModelIndex, int, int)),
		    this, SLOT(watchAllStoredMessageList()));
		connect(m_ui->allMessagesView->model(),
		    SIGNAL(rowsRemoved(QModelIndex, int, int)),
		    this, SLOT(watchAllStoredMessageList()));

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

		connect(m_ui->allStoredScanPushButton, SIGNAL(clicked()),
		    this, SLOT(startAccountScan()));
		connect(m_ui->allStoredScanInterruptPushButton, SIGNAL(clicked()),
		    this, SLOT(interruptAccountScan()));

		connect(m_ui->allCheckBox, SIGNAL(stateChanged(int)),
		    this, SLOT(applyAllSelection(int)));
		connect(&m_accountSelectionModel,
		    SIGNAL(dataChanged(QModelIndex, QModelIndex)),
		    this, SLOT(modifiedAccountSelection()));

		m_ui->allMessagesFilterHorizontalLayout->setVisible(false);
		connect(m_ui->allMessagesFilterLine, SIGNAL(textChanged(QString)),
		    this, SLOT(filterAllMessages(QString)));
		m_ui->closeAllFilterButton->setIcon(style()->standardIcon(QStyle::SP_DockWidgetCloseButton));
		connect(m_ui->closeAllFilterButton, SIGNAL(clicked()),
		    this, SLOT(hideAllMessagesFilter()));

		connect(m_ui->onlyRestampableCheckBox, SIGNAL(stateChanged(int)),
		    this, SLOT(applyRestampableFiltering(int)));
		m_ui->onlyRestampableCheckBox->setCheckState(Qt::Checked);

		connect(m_ui->restampSelectedAllStoredPushButton, SIGNAL(clicked()),
		    this, SLOT(renewSelectedAllStoredTimestamps()));
		connect(m_ui->restampListedAllStoredPushButton, SIGNAL(clicked()),
		    this, SLOT(renewListedAllStoredTimestamps()));

		connect(&m_scanAccountsThread, SIGNAL(stopped(bool)),
		    this, SLOT(watchScanStopped(bool)),
		    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */
		connect(&m_scanAccountsThread, SIGNAL(processed(AcntIdDb, QList<TstEntry>)),
		    this, SLOT(watchScanProcessed(AcntIdDb, QList<TstEntry>)),
		    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */
		connect(&m_scanAccountsThread, SIGNAL(progress(AcntIdDb, int, int)),
		    this, SLOT(watchScanProgress(AcntIdDb, int, int)),
		    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */
	}

	/* external */
	{
		/* Set accounts into combobox - account name and user name. */
		for (const AcntIdDb &acntIdDb : m_acntIdDbList) {
			const AcntId aId(acntIdDb);
			const QString accountName =
			    GlobInstcs::acntMapPtr->acntData(aId).accountName() +
			    " (" + acntIdDb.username() + ")";
			m_ui->accountComboBox->addItem(accountName, QVariant::fromValue(aId));
		}

		connect(m_ui->chooseSrcPushButton, SIGNAL(clicked()),
		    this, SLOT(selectExternalSrcZfo()));

		connect(m_ui->viewSrcPushButton, SIGNAL(clicked()),
		    this, SLOT(viewExternalSrcZfo()));

		connect(m_ui->chooseTgtPushButton, SIGNAL(clicked()),
		    this, SLOT(selectExternalTgtZfo()));

		connect(m_ui->restampExternalPushButton, SIGNAL(clicked()),
		    this, SLOT(renewExternalTimestamps()));

		m_ui->srcZfoLineEdit->setReadOnly(true);
		m_ui->tgtZfoLineEdit->setReadOnly(true);

		/* Enable only if having available accounts. */
		m_ui->chooseSrcPushButton->setEnabled(m_ui->accountComboBox->count() > 0);

		m_ui->chooseSrcPushButton->setEnabled(true);
		m_ui->viewSrcPushButton->setEnabled(false);

		m_ui->tgtZfoLineEdit->setEnabled(false);
		m_ui->chooseTgtPushButton->setEnabled(false);

		m_ui->restampExternalPushButton->setEnabled(false);

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

		m_ui->singleResultLabel->setText(QString());
	}

	updateActionActivation();

	menuConnectActions();
	databaseConnectActions();
	workerConnectActions();
}

void DlgTimestampRenewal::menuConnectActions(void)
{
	connect(m_ui->actionTrackedSelectedRestamp, SIGNAL(triggered()),
	    this, SLOT(renewSelectedTrackedTimestamps()));
	connect(m_ui->actionTrackedTrackTimestampExpiration, SIGNAL(triggered(bool)),
	    this, SLOT(toggleTrackedTimestampTracking(bool)));

	connect(m_ui->actionAllStoredSelectedRestamp, SIGNAL(triggered()),
	    this, SLOT(renewSelectedAllStoredTimestamps()));
	connect(m_ui->actionAllStoredTrackTimestampExpiration, SIGNAL(triggered(bool)),
	    this, SLOT(toggleAllStoredTimestampTracking(bool)));
}

void DlgTimestampRenewal::databaseConnectActions(void)
{
	if (Q_NULLPTR != m_timestampDb) {
		connect(m_timestampDb, SIGNAL(identificationsInserted(Json::MsgId2List, AcntId)),
		    this, SLOT(watchIdentificationsInserted(Json::MsgId2List, AcntId)));
		connect(m_timestampDb, SIGNAL(identificationsDeleted(Json::MsgId2List)),
		    this, SLOT(watchIdentificationsDeleted(Json::MsgId2List)));
		connect(m_timestampDb, SIGNAL(msgTimestampDataUpdated(TstValidityHash)),
		    this, SLOT(watchMsgTimestampDataUpdated(TstValidityHash)),
		    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */
	}

	connect(GlobInstcs::prefsPtr, SIGNAL(entrySet(int, QString, QVariant)),
	    this, SLOT(watchPrefsModification(int, QString, QVariant)));
}

void DlgTimestampRenewal::workerConnectActions(void)
{
	connect(GlobInstcs::msgProcEmitterPtr, SIGNAL(uploadProgress(AcntId, QString, qint64, qint64)),
	    this, SLOT(watchUploadProgress(AcntId, QString, qint64, qint64)),
	    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */

	connect(GlobInstcs::msgProcEmitterPtr, SIGNAL(uploadProgressFinished(AcntId, QString, int, QString, QVariant)),
	    this, SLOT(watchUploadProgressFinished(AcntId, QString, int, QString, QVariant)),
	    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */

	connect(GlobInstcs::msgProcEmitterPtr, SIGNAL(downloadProgress(AcntId, QString, MsgId, qint64, qint64)),
	    this, SLOT(watchDownloadProgress(AcntId, QString, MsgId, qint64, qint64)),
	    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */

	connect(GlobInstcs::msgProcEmitterPtr, SIGNAL(downloadProgressFinished(AcntId, QString, MsgId, int, QString)),
	    this, SLOT(watchDownloadProgressFinished(AcntId, QString, MsgId, int, QString)),
	    Qt::QueuedConnection); /* Qt::QueuedConnection because it must be executed in receiver's thread. */
}

void DlgTimestampRenewal::updateActionActivation(void)
{
	const bool noTasks = m_tasks.isEmpty() && (Q_NULLPTR == m_externalTask);
	const bool noScanTask = !m_scanAccountsThread.isRunning();

	/* tracked */
	{
		const bool enabled = m_trackedModelRowSelection.size() > 0;

		m_ui->restampSelectedTrackedPushButton->setEnabled(enabled && noTasks);

		m_ui->actionTrackedSelectedRestamp->setEnabled(enabled && noTasks);

		/* Check state is determined when shown in context menu. */
		//m_ui->actionTrackedTrackTimestampExpiration->setChecked(enabled);
		m_ui->actionTrackedTrackTimestampExpiration->setEnabled(enabled);
	}

	/* all stored */
	{
		const bool acntSelected = m_ui->allCheckBox->checkState() != Qt::Unchecked;
		const bool msgsListed = m_allStoredSortProxyModel.rowCount() > 0;
		const bool msgsSelected = m_allStoredModelRowSelection.size() > 0;

		m_ui->allCheckBox->setEnabled(noScanTask);
		m_ui->accountView->setEnabled(noScanTask);
		m_ui->allMessagesView->setEnabled(noScanTask);

		m_ui->onlyRestampableCheckBox->setEnabled(noScanTask);
		m_ui->allStoredScanPushButton->setEnabled(acntSelected && noTasks && noScanTask);
		m_ui->allStoredScanInterruptPushButton->setEnabled(!noScanTask);

		m_ui->restampSelectedAllStoredPushButton->setEnabled(msgsSelected && noTasks && noScanTask);

		m_ui->actionAllStoredSelectedRestamp->setEnabled(msgsSelected && noTasks && noScanTask);

		/* Check state is determined when shown in context menu. */
		//m_ui->actionAllStoredTrackTimestampExpiration->setChecked(msgsSelected);
		m_ui->actionAllStoredTrackTimestampExpiration->setEnabled(msgsSelected);

		m_ui->restampListedAllStoredPushButton->setEnabled(msgsListed && noTasks && noScanTask);
	}

	/* external */
	{
		const bool enabled = (m_ui->accountComboBox->count() > 0)
		    && (Q_NULLPTR == m_externalTask);

		m_ui->chooseSrcPushButton->setEnabled(enabled);

		m_ui->viewSrcPushButton->setEnabled(enabled && (!m_ui->srcZfoLineEdit->text().isEmpty()));

		m_ui->tgtZfoLineEdit->setEnabled(enabled && (!m_ui->srcZfoLineEdit->text().isEmpty()));
		m_ui->chooseTgtPushButton->setEnabled(enabled && (!m_ui->srcZfoLineEdit->text().isEmpty()));

		m_ui->restampExternalPushButton->setEnabled(enabled
		    && (!m_ui->srcZfoLineEdit->text().isEmpty())
		    && (!m_ui->tgtZfoLineEdit->text().isEmpty())
		    && noTasks);
	}
}

void DlgTimestampRenewal::renewSelectedTimestamps(
    const QList<int> &modelRowSelection,
    MsgTstListingModel &timestampTableModel)
{
	const QString timeStr = QDateTime::currentDateTimeUtc().toString();

	m_ui->restampSelectedTrackedPushButton->setEnabled(false);
	m_ui->actionTrackedSelectedRestamp->setEnabled(false);
	m_ui->restampSelectedAllStoredPushButton->setEnabled(false);
	m_ui->actionAllStoredSelectedRestamp->setEnabled(false);
	m_ui->restampListedAllStoredPushButton->setEnabled(false);
	m_ui->restampExternalPushButton->setEnabled(false);
	this->setCursor(Qt::BusyCursor);

	QList<MsgOrigin> originList = messageOriginList(modelRowSelection,
	    timestampTableModel, m_sessions, m_mw);
	if (Q_UNLIKELY(originList.isEmpty())) {
		updateActionActivation();
		this->setCursor(Qt::ArrowCursor);
	}

	for (const MsgOrigin &msgOrigin : originList) {
		const QString transactionId = QString("%1_%2_%3_%4")
		    .arg(msgOrigin.acntIdDb.username())
		    .arg(timeStr)
		    .arg(msgOrigin.msgId.dmId())
		    .arg(Utility::generateRandomString(6));

		timestampTableModel.updateError(msgOrigin.acntIdDb, msgOrigin.msgId, QString());

		TaskArchiveIsdsDocument *task = new (::std::nothrow) TaskArchiveIsdsDocument(
		    msgOrigin.acntIdDb, transactionId,
		    msgOrigin.msgId, msgOrigin.direction);
		if (Q_UNLIKELY(task == Q_NULLPTR)) {
			Q_ASSERT(0);
			break;
		}

		m_trackedTransactionMap[transactionId] = QPair<AcntId, MsgId>(msgOrigin.acntIdDb, msgOrigin.msgId);
		m_tasks[transactionId] = task;

		task->setAutoDelete(false);
		GlobInstcs::workPoolPtr->assignLo(task);

		/* Tasks are deleted inside watchDownloadProgressFinished(). */
	}

	/* Actions are activated inside watchDownloadProgressFinished(). */
}

void DlgTimestampRenewal::messageItemDoubleClicked(const QModelIndex &index,
    const QList<const QAbstractProxyModel *> &proxyModelCascade,
    const MsgTstListingModel &timestampTableModel)
{
	if (Q_UNLIKELY(!index.isValid())) {
		return;
	}

	QModelIndex srcIdx = transitiveMapToSource({index}, proxyModelCascade)
	    .first();
	if (Q_UNLIKELY(!srcIdx.isValid())) {
		Q_ASSERT(0);
		return;
	}

	const TstEntry &tstEntry = timestampTableModel.tstEntry(srcIdx.row());
	const MessageDb::SoughtMsg &msgEntry = timestampTableModel.msgEntry(srcIdx.row());

	const AcntId acntId(tstEntry.username, (tstEntry.msgId.testEnv() == Isds::Type::BOOL_TRUE));

	if (Q_UNLIKELY((!acntId.isValid()) || (!msgEntry.isValid()))) {
		return;
	}

	Q_EMIT focusSelectedMsg(acntId, tstEntry.msgId.dmId(),
	    MessageDbSet::yearFromDateTime(tstEntry.msgId.deliveryTime()),
	    (int)msgEntry.type);
	/* Don't close the dialogue. */
}

void DlgTimestampRenewal::clearFilterLineViaFilter(QObject *lineEditPtr)
{
	if (Q_UNLIKELY(Q_NULLPTR == lineEditPtr)) {
		return;
	}

	QLineEdit *lineEdit = qobject_cast<QLineEdit *>(lineEditPtr);
	if (Q_UNLIKELY(Q_NULLPTR == lineEdit)) {
		Q_ASSERT(0);
		return;
	}

	lineEdit->clear();
}

void DlgTimestampRenewal::showTrackedFilterLineViaFilter(QObject *dlgPtr)
{
	if (Q_UNLIKELY(Q_NULLPTR == dlgPtr)) {
		return;
	}

	DlgTimestampRenewal *dlg = qobject_cast<DlgTimestampRenewal *>(dlgPtr);
	if (Q_UNLIKELY(Q_NULLPTR == dlg)) {
		Q_ASSERT(0);
		return;
	}
	if (dlg->m_ui->trackedMessagesFilterLine != Q_NULLPTR) {
		showFilterLine(dlg->m_ui->trackedMessagesFilterHorizontalLayout,
		    dlg->m_ui->trackedMessagesFilterLine);
	}
}

void DlgTimestampRenewal::showAllFilterLineViaFilter(QObject *dlgPtr)
{
	if (Q_UNLIKELY(Q_NULLPTR == dlgPtr)) {
		return;
	}

	DlgTimestampRenewal *dlg = qobject_cast<DlgTimestampRenewal *>(dlgPtr);
	if (Q_UNLIKELY(Q_NULLPTR == dlg)) {
		Q_ASSERT(0);
		return;
	}
	if (dlg->m_ui->allMessagesFilterLine != Q_NULLPTR) {
		showFilterLine(dlg->m_ui->allMessagesFilterHorizontalLayout,
		    dlg->m_ui->allMessagesFilterLine);
	}
}

void DlgTimestampRenewal::hideTrackedFilterLineViaFilter(QObject *dlgPtr)
{
	if (Q_UNLIKELY(Q_NULLPTR == dlgPtr)) {
		return;
	}

	DlgTimestampRenewal *dlg = qobject_cast<DlgTimestampRenewal *>(dlgPtr);
	if (Q_UNLIKELY(Q_NULLPTR == dlg)) {
		Q_ASSERT(0);
		return;
	}
	if (dlg->m_ui->trackedMessagesFilterLine != Q_NULLPTR) {
		hideFilterLine(dlg->m_ui->trackedMessagesFilterHorizontalLayout,
		    dlg->m_ui->trackedMessagesView);
	}
}

void DlgTimestampRenewal::hideAllFilterLineViaFilter(QObject *dlgPtr)
{
	if (Q_UNLIKELY(Q_NULLPTR == dlgPtr)) {
		return;
	}

	DlgTimestampRenewal *dlg = qobject_cast<DlgTimestampRenewal *>(dlgPtr);
	if (Q_UNLIKELY(Q_NULLPTR == dlg)) {
		Q_ASSERT(0);
		return;
	}
	if (dlg->m_ui->allMessagesFilterLine != Q_NULLPTR) {
		hideFilterLine(dlg->m_ui->allMessagesFilterHorizontalLayout,
		    dlg->m_ui->allMessagesView);
	}
}

void DlgTimestampRenewal::focusOnTrackedMessageViaFilter(QObject *dlgPtr)
{
	if (Q_UNLIKELY(dlgPtr == Q_NULLPTR)) {
		return;
	}

	DlgTimestampRenewal *dlg = qobject_cast<DlgTimestampRenewal *>(dlgPtr);
	if (Q_UNLIKELY(dlg == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}

	LoweredTableView *view = dlg->m_ui->trackedMessagesView;
	if (Q_UNLIKELY(Q_NULLPTR == view)) {
		Q_ASSERT(0);
		return;
	}

	const QModelIndexList indexes =
	    view->selectionModel()->selectedRows(MsgTstListingModel::COL_ACCOUNT_ID);
	/* Focus on message only when one message selected. */
	if (indexes.size() != 1) {
		return;
	}

	dlg->messageItemDoubleClicked(indexes.first(),
	    {&(dlg->m_trackedSortProxyModel), &(dlg->m_trackedProgressProxyModel)},
	    dlg->m_trackedTimestampTableModel);
}

void DlgTimestampRenewal::focusOnAllMessageViaFilter(QObject *dlgPtr)
{
	if (Q_UNLIKELY(dlgPtr == Q_NULLPTR)) {
		return;
	}

	DlgTimestampRenewal *dlg = qobject_cast<DlgTimestampRenewal *>(dlgPtr);
	if (Q_UNLIKELY(dlg == Q_NULLPTR)) {
		Q_ASSERT(0);
		return;
	}

	LoweredTableView *view = dlg->m_ui->allMessagesView;
	if (Q_UNLIKELY(Q_NULLPTR == view)) {
		Q_ASSERT(0);
		return;
	}

	const QModelIndexList indexes =
	    view->selectionModel()->selectedRows(MsgTstListingModel::COL_ACCOUNT_ID);
	/* Focus on message only when one message selected. */
	if (indexes.size() != 1) {
		return;
	}

	dlg->messageItemDoubleClicked(indexes.first(),
	    {&(dlg->m_allStoredSortProxyModel), &(dlg->m_allStoredTimeProxyModel), &(dlg->m_allStoredProgressProxyModel)},
	    dlg->m_allStoredTimestampTableModel);
}
