/*
 * Copyright (C) 2014-2022 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 <QMutableListIterator>
#include <QPushButton>
#include <QSet>

#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/worker/pool.h"
#include "src/global.h"
#include "src/gui/datovka.h"
#include "src/gui/dlg_download_messages.h"
#include "src/gui/dlg_msg_box_informative.h"
#include "src/identifiers/account_id_db.h"
#include "src/identifiers/message_id.h"
#include "src/io/isds_sessions.h"
#include "src/views/table_home_end_filter.h"
#include "src/views/table_tab_ignore_filter.h"
#include "src/worker/task_download_message.h"
#include "ui_dlg_download_messages.h"

MsgOrigin MsgOriginAndUpladTarget::toMsgOrigin(void) const
{
	return MsgOrigin(acntIdDb, msgId, direction, downloaded);
}

MsgOriginAndUpladTarget MsgOriginAndUpladTarget::fromMsgOrigin(
    const MsgOrigin &origin)
{
	return MsgOriginAndUpladTarget(origin.acntIdDb, origin.msgId,
	    origin.direction, origin.downloaded,
	    Task::PROC_LIST_SCHEDULED_LAST | Task::PROC_NO_ERROR_DLG,
	    QString());
}

QList<MsgOriginAndUpladTarget> MsgOriginAndUpladTarget::fromMsgOriginList(
    const QList<MsgOrigin> &originList)
{
	QList<MsgOriginAndUpladTarget> originsAndTargets;

	foreach (const MsgOrigin &origin, originList) {
		originsAndTargets.append(fromMsgOrigin(origin));
	}

	return originsAndTargets;
}

QList<MsgOrigin> MsgOriginAndUpladTarget::toMsgOriginList(
    const QList<MsgOriginAndUpladTarget> &oautList)
{
	QList<MsgOrigin> originList;

	foreach (const MsgOriginAndUpladTarget &oaut, oautList) {
		originList.append(oaut.toMsgOrigin());
	}

	return originList;
}

MessageDownloadThread::MessageDownloadThread(
    const QList<MsgOriginAndUpladTarget> &originAndTargetList,
    volatile bool &stop)
    : m_originAndTargetList(originAndTargetList),
    m_stop(stop)
{
}

void MessageDownloadThread::run(void)
{
	logDebugLv0NL("Starting message download in thread '%p'.",
	    (void *) QThread::currentThreadId());

	int i = 0;
	const int total = m_originAndTargetList.size();

	emit started(i, total);

	bool interrupted = false;
	foreach (const MsgOriginAndUpladTarget &oaut, m_originAndTargetList) {
		if (m_stop) {
			interrupted = true;
			break;
		}
		TaskDownloadMessage *task = new (::std::nothrow) TaskDownloadMessage(
		    oaut.acntIdDb, oaut.direction, oaut.msgId, oaut.taskFlags,
		    oaut.recMgmtHierarchyId);
		if (Q_UNLIKELY(task == Q_NULLPTR)) {
			Q_ASSERT(0);
			break;
		}
		task->setAutoDelete(false);
		GlobInstcs::workPoolPtr->runSingle(task);
		bool ret = TaskDownloadMessage::DM_SUCCESS == task->m_result;
		/* Message identifier might have changed. */
		MsgId newMsgId = oaut.msgId;
		if (ret) {
			newMsgId.setDeliveryTime(task->m_mId.deliveryTime());
		}

		emit processed(i, total, oaut.acntIdDb, newMsgId, ret,
		    task->m_isdsError + " " + task->m_isdsLongError);

		delete task;
		++i;
	}

	emit stopped(interrupted);

	logDebugLv0NL("Message download finished in thread '%p'.",
	    (void *) QThread::currentThreadId());
}

DlgDownloadMessages::DlgDownloadMessages(
    const QList<MsgOriginAndUpladTarget> &originAndTargetList,
    enum FinalProposal finalProposal, QWidget *parent)
    : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgDownloadMessages),
    m_finalProposal(finalProposal),
    m_downloadListProxyModel(this),
    m_downloadModel(this),
    m_breakDownloadLoop(false),
    m_downloadThread(originAndTargetList, m_breakDownloadLoop)
{
	m_ui->setupUi(this);

	initDialogue();

	/* Load model content. */
	foreach (const MsgOriginAndUpladTarget &oaut, originAndTargetList) {
		m_downloadModel.appendData(oaut.acntIdDb, oaut.msgId,
		    MsgDownloadModel::STAT_NOT_ATTEMPTED, QString());
	}
}

DlgDownloadMessages::~DlgDownloadMessages(void)
{
	m_downloadThread.wait();
	delete m_ui;
}

/*!
 * @brief Check whether list contains matching account and message identifier.
 *
 * @note Does not compare delivery date as this value may change.
 *
 * @param[in] originList List of entries.
 * @param[in] acntIdDb Sought account identifier.
 * @param[in] dmId Message identifier.
 * @return Non negative index of found entry if found, -1 else.
 */
static
int containsEntry(const QList<MsgOrigin> &originList, const AcntIdDb &acntIdDb,
    qint64 dmId)
{
	for (int i = 0; i < originList.size(); ++i) {
		const MsgOrigin &origin = originList.at(i);
		if ((origin.acntIdDb == acntIdDb) &&
		    (origin.msgId.dmId() == dmId)) {
			return i;
		}
	}

	return -1;
}

/*!
 * @brief Check whether list contains matching account and message identifier.
 *
 * @note Does not compare delivery date as this value may change.
 *
 * @param[in] originAndTargetList List of message identifiers and
 *                                upload targets.
 * @param[in] acntIdDb Sought account identifier.
 * @param[in] dmId Message identifier.
 * @return Non negative index of found entry if found, -1 else.
 */
static
int containsEntry(const QList<MsgOriginAndUpladTarget> &originAndTargetList,
    const AcntIdDb &acntIdDb, qint64 dmId)
{
	for (int i = 0; i < originAndTargetList.size(); ++i) {
		const MsgOriginAndUpladTarget &oaut = originAndTargetList.at(i);
		if ((oaut.acntIdDb == acntIdDb) && (oaut.msgId.dmId() == dmId)) {
			return i;
		}
	}

	return -1;
}

QList<MsgOrigin> DlgDownloadMessages::download(
    const QList<MsgOriginAndUpladTarget> &originAndTargetList,
    enum FinalProposal finalProposal, bool &abort, QWidget *parent)
{
	if (Q_UNLIKELY(originAndTargetList.isEmpty())) {
		return QList<MsgOrigin>();
	}

	DlgDownloadMessages dlg(originAndTargetList, finalProposal, parent);

	dlg.m_downloadThread.start();
	if (QDialog::Accepted == dlg.exec()) {
		/* Collect all successfully downloaded messages. */
		QList<MsgOrigin> downloadedList;
		typedef QPair<AcntIdDb, MsgId> IdPair;
		foreach (const IdPair &pair, dlg.m_downloadModel.downloaded()) {
			int i = containsEntry(originAndTargetList,
			    pair.first, pair.second.dmId());
			if (i >= 0) {
				/* Update delivery time. */
				MsgOrigin entry = originAndTargetList.at(i);
				entry.msgId = pair.second;
				entry.downloaded = true;
				downloadedList.append(entry);
			}
		}
		abort = false;
		return downloadedList;
	}

	abort = true;
	return QList<MsgOrigin>();
}

/*!
 * @brief Check whether there are downloaded messages.
 *
 * @param[in] originAndTargetList List of message identifiers and
 *                                upload targets.
 * @return True if there are some which are downloaded.
 */
static
bool containsDownloaded(
    const QList<MsgOriginAndUpladTarget> &originAndTargetList)
{
	foreach (const MsgOriginAndUpladTarget &oaut, originAndTargetList) {
		if (oaut.downloaded) {
			return true;
		}
	}

	return false;
}

/*!
 * @brief Check whether there are not downloaded messages.
 *
 * @param[in] originAndTargetList List of message identifiers and
 *                                upload targets.
 * @return True if there are some which are not downloaded.
 */
static
bool containsUndownloaded(
    const QList<MsgOriginAndUpladTarget> &originAndTargetList)
{
	foreach (const MsgOriginAndUpladTarget &oaut, originAndTargetList) {
		if (!oaut.downloaded) {
			return true;
		}
	}

	return false;
}

/*!
 * @brief Return list of not downloaded messages.
 *
 * @param[in] originAndTargetList List of message identifiers and
 *                                upload targets.
 * @return List of messages that are not downloaded.
 */
static
QList<MsgOriginAndUpladTarget> undownloaded(
    const QList<MsgOriginAndUpladTarget> &originAndTargetList)
{
	QList<MsgOriginAndUpladTarget> notDownloaded;
	foreach (const MsgOriginAndUpladTarget &oaut, originAndTargetList) {
		if (!oaut.downloaded) {
			notDownloaded.append(oaut);
		}
	}

	return notDownloaded;
}

/*!
 * @brief Brief return set of all account identifiers.
 *
 * @param[in] originAndTargetList List of message identifiers and
 *                                upload targets.
 * @return Set of account identifiers.
 */
static
QSet<AcntIdDb> originAcntIds(
    const QList<MsgOriginAndUpladTarget> &originAndTargetList)
{
	QSet<AcntIdDb> acntSet;
	foreach (const MsgOriginAndUpladTarget &oaut, originAndTargetList) {
		acntSet.insert(oaut.acntIdDb);
	}
	return acntSet;
}

void DlgDownloadMessages::downloadAll(
    const QList<MsgOriginAndUpladTarget> &originAndTargetList, MainWindow *mw,
    QWidget *parent)
{
	if (Q_UNLIKELY(originAndTargetList.isEmpty())) {
		return;
	}

	/* Construct list of all downloaded messages. */
	QList<MsgOriginAndUpladTarget> notDownloadedYet = originAndTargetList;

	if (containsDownloaded(originAndTargetList)) {
		QString detailText =
		    tr("The content of the following messages has already been downloaded:");
		{
			QStringList idList;
			foreach (const MsgOriginAndUpladTarget &oaut, originAndTargetList) {
				if (oaut.downloaded) {
					idList.append(QString::number(
					    oaut.msgId.dmId()));
				}
			}
			detailText += " " + idList.join(", ");
		}

		int res = DlgMsgBox::message(parent, QMessageBox::Warning,
		    tr("Message Content Available"),
		    tr("The content of some messages is already available."),
		    tr("You don't have to download the content of those messages again.") +
		    QLatin1String("\n\n") +
		    tr("Do you also want to download the content of the already downloaded messages?"),
		    detailText,
		    QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);

		if (res == QMessageBox::No) {
			/* Take not downloaded only. */
			notDownloadedYet = undownloaded(originAndTargetList);
		}
	}

	/* Try connection to all required accounts. */
	foreach (const AcntIdDb &acntIdDb, originAcntIds(notDownloadedYet)) {
		if (!GlobInstcs::isdsSessionsPtr->isConnectedToIsds(acntIdDb.username())) {
			if (mw != Q_NULLPTR) {
				mw->connectToIsds(acntIdDb);
			}
		}
	}

	/* Remove those where the login procedure failed. */
	{
		QMutableListIterator<MsgOriginAndUpladTarget> it(notDownloadedYet);
		while (it.hasNext()) {
			const QString username = it.next().acntIdDb.username();
			if (!GlobInstcs::isdsSessionsPtr->isConnectedToIsds(username)) {
				it.remove();
			}
		}
	}

	bool abort = false;
	download(notDownloadedYet, ACCEPT_INHIBIT, abort, parent);
}

QList<MsgOrigin> DlgDownloadMessages::offerDownloadForMissing(
    const QList<MsgOriginAndUpladTarget> &originAndTargetList, MainWindow *mw,
    QWidget *parent)
{
	if (Q_UNLIKELY(originAndTargetList.isEmpty())) {
		return QList<MsgOrigin>();
	}

	if (!containsUndownloaded(originAndTargetList)) {
		/* All complete messages should be already downloaded. */
		return MsgOriginAndUpladTarget::toMsgOriginList(
		    originAndTargetList);
	}

	{
		QString detailText =
		    tr("The content of the following messages has not been completely downloaded:");
		{
			QStringList idList;
			foreach (const MsgOriginAndUpladTarget &oaut, originAndTargetList) {
				if (!oaut.downloaded) {
					idList.append(QString::number(
					    oaut.msgId.dmId()));
				}
			}
			detailText += " " + idList.join(", ");
		}

		int res = DlgMsgBox::message(parent, QMessageBox::Warning,
		    tr("Message Content Not Available"),
		    tr("The content of some messages is not available."),
		    tr("Messages with missing content will be skipped.") +
		    QLatin1String("\n\n") +
		    tr("Do you want to download the missing message content now?"),
		    detailText,
		    QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);

		if (res == QMessageBox::No) {
			/* Remove messages with missing content. */
			QList<MsgOrigin> list;
			foreach (const MsgOrigin &origin, originAndTargetList) {
				if (origin.downloaded) {
					list.append(origin);
				}
			}
			return list;
		}
	}

	/* Construct list of all downloaded messages. */
	QList<MsgOrigin> downloadedList =
	    MsgOriginAndUpladTarget::toMsgOriginList(originAndTargetList);
	{
		QList<MsgOriginAndUpladTarget> notDownloadedYet = undownloaded(originAndTargetList);
		/* Try connection to all required accounts. */
		foreach (const AcntIdDb &acntIdDb, originAcntIds(notDownloadedYet)) {
			if (!GlobInstcs::isdsSessionsPtr->isConnectedToIsds(acntIdDb.username())) {
				if (mw != Q_NULLPTR) {
					mw->connectToIsds(acntIdDb);
				}
			}
		}

		/* Remove those where the login procedure failed. */
		{
			QMutableListIterator<MsgOriginAndUpladTarget> it(notDownloadedYet);
			while (it.hasNext()) {
				const QString username = it.next().acntIdDb.username();
				if (!GlobInstcs::isdsSessionsPtr->isConnectedToIsds(username)) {
					it.remove();
				}
			}
		}

		{
			bool abort = false;
			const QList<MsgOrigin> justDownloaded = download(
			    notDownloadedYet,
			    containsDownloaded(originAndTargetList) ?
			        ACCEPT_ALLOW : ACCEPT_ON_SOME_DOWNLOADED,
			    abort, parent);
			if (Q_UNLIKELY(abort)) {
				return QList<MsgOrigin>();
			}
			/* Set downloaded flag for all just downloaded messages. */
			foreach (const MsgOrigin &downloaded, justDownloaded) {
				int i = containsEntry(downloadedList,
				    downloaded.acntIdDb,
				    downloaded.msgId.dmId());
				if (i >= 0) {
					/* Also update delivery time. */
					downloadedList[i].msgId = downloaded.msgId;
					downloadedList[i].downloaded = true;
				}
			}
		}

		/* Delete all that could not be downloaded. */
		{
			QMutableListIterator<MsgOrigin> it(downloadedList);
			while (it.hasNext()) {
				if (!it.next().downloaded) {
					it.remove();
				}
			}
		}
	}

	return downloadedList;
}

void DlgDownloadMessages::interruptDownload(void)
{
	m_ui->statusLabel->setText(tr("Interrupting download."));
	m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false);
	if (!m_downloadThread.isFinished()) {
		m_breakDownloadLoop = true;
	} else {
		/* Close the dialogue when no download running. */
		reject();
	}
}

void DlgDownloadMessages::downloadStarted(int i, int total)
{
	m_ui->downloadProgress->setMinimum(i);
	m_ui->downloadProgress->setMaximum(total);
	m_ui->downloadProgress->setValue(i);
}

void DlgDownloadMessages::downloadStopped(bool interrupted)
{
	if (!interrupted) {
		if (m_downloadModel.allDownloaded()) {
			/* Close the dialogue by accepting it automatically. */
			accept();
			return;
		}

		if (m_downloadModel.someDownloaded()) {
			/* Let the user decide whether to proceed. */
			QString message =
			    tr("Some messages could not be downloaded.");
			if (m_finalProposal != ACCEPT_INHIBIT) {
				message += QLatin1String("\n") +
				    tr("Do you want to proceed using all available messages?");
			}
			m_ui->statusLabel->setText(message);
			m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
			    m_finalProposal != ACCEPT_INHIBIT);
			m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true);
			return;
		}
	} else {
		/* Download has been interrupted. */
		if (m_downloadModel.someDownloaded()) {
			/* Let the user decide whether to proceed. */
			QString message = tr("Download has been interrupted.");
			if (m_finalProposal != ACCEPT_INHIBIT) {
				message += QLatin1String("\n") +
				    tr("Do you want to proceed using the available messages?");
			}
			m_ui->statusLabel->setText(message);
			m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
			    m_finalProposal != ACCEPT_INHIBIT);
			m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true);
			return;
		}
	}

	/*
	 * !m_downloadModel.someDownloaded()
	 * Notify the user.
	 * There still may be messages already downloaded
	 * which the user wants to process.
	 */
	QString message = tr("No messages have been downloaded.");
	if (m_finalProposal == ACCEPT_ALLOW) {
		message += QLatin1String("\n") +
		    tr("There are other already downloaded messages. Do you want to proceed?");
	}
	m_ui->statusLabel->setText(message);
	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(
	    m_finalProposal == ACCEPT_ALLOW);
	m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(true);
}

void DlgDownloadMessages::downloadedMessage(int i, int total,
    const AcntIdDb &acntIdDb, const MsgId &msgId,
    bool success, const QString &error)
{
	Q_UNUSED(total);

	m_ui->downloadProgress->setValue(i + 1); /* Indexed from 0. */

	m_downloadModel.updateDownloadState(acntIdDb, msgId,
	    success ? MsgDownloadModel::STAT_SUCCEEDED : MsgDownloadModel::STAT_FAILED,
	    error);
}

void DlgDownloadMessages::initDialogue(void)
{
	m_ui->downloadView->setNarrowedLineHeight();
	m_ui->downloadView->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
	m_ui->downloadView->setSelectionMode(QAbstractItemView::NoSelection);
	m_ui->downloadView->setSelectionBehavior(QAbstractItemView::SelectRows);

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

	m_downloadListProxyModel.setSortRole(MsgDownloadModel::ROLE_PROXYSORT);
	m_downloadListProxyModel.setSourceModel(&m_downloadModel);

	m_ui->downloadView->setModel(&m_downloadListProxyModel);

	m_ui->downloadView->setSortingEnabled(true);
	m_ui->downloadView->sortByColumn(MsgDownloadModel::COL_MSG_ID, Qt::AscendingOrder);

	m_ui->downloadView->setSquareColumnWidth(MsgDownloadModel::COL_STATE);

	downloadStarted(0, 100);

	m_ui->statusLabel->setText(QString());

	connect(m_ui->buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
	connect(m_ui->buttonBox, SIGNAL(rejected()), this, SLOT(interruptDownload()));

	connect(&m_downloadThread, SIGNAL(started(int, int)),
	    this, SLOT(downloadStarted(int, int)));
	connect(&m_downloadThread, SIGNAL(stopped(bool)),
	    this, SLOT(downloadStopped(bool)));
	connect(&m_downloadThread, SIGNAL(processed(int, int, AcntIdDb, MsgId, bool, QString)),
	    this, SLOT(downloadedMessage(int, int, AcntIdDb, MsgId, bool, QString)));

	/* Disable accept button. */
	m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
}
