/*
 * 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 <cinttypes> /* PRId64 */
#include <QDir>
#include <QFont>
#include <QMimeData>
#include <QSet>
#include <QStringBuilder>
#include <QUrl>

#include "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/datovka_shared/graphics/graphics.h"
#include "src/datovka_shared/io/db_time.h"
#include "src/datovka_shared/io/filesystem.h"
#include "src/datovka_shared/io/message_db_tables.h"
#include "src/datovka_shared/io/records_management_db.h"
#include "src/datovka_shared/isds/type_conversion.h"
#include "src/datovka_shared/isds/type_description.h"
#include "src/datovka_shared/json/basic.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/utility/date_time.h"
#include "src/delegates/tag_item.h"
#include "src/global.h"
#include "src/gui/icon_container.h"
#include "src/io/exports.h"
#include "src/io/filesystem.h"
#include "src/io/message_db.h"
#include "src/io/message_db_set.h"
#include "src/io/tag_container.h"
#include "src/io/timestamp_db.h" /* TstValidityHash */
#include "src/json/message_id.h"
#include "src/json/note_assignment_hash.h"
#include "src/json/note_entry.h"
#include "src/json/tag_assignment_hash.h"
#include "src/models/helper.h"
#include "src/models/messages_model.h"

static IconContainer inconContainer; /* Local icon container. */

/*!
 * @brief Construct the default records management icon.
 *
 * @return Icon object.
 */
static inline
QIcon defaultRMIcon(void)
{
	return IconContainer::construcIcon(IconContainer::ICON_APP_ARROW_UP);
}

DbMsgsTblModel::DbMsgsTblModel(enum DbMsgsTblModel::Type type, QObject *parent)
    : TblModel(parent),
    m_acntIdDb(),
    m_type(type),
    m_rmIco(defaultRMIcon()),
    m_msgDltNotifyAheadDays(0),
    m_msgDltNotifyColour()
{
	/* Fixed column size. */
	m_columnCount = MAX_COLNUM;
}

#define unacceptedState(state) \
	((state) < Isds::Type::MS_ACCEPTED_FICT)

QVariant DbMsgsTblModel::data(const QModelIndex &index, int role) const
{
	/* Draw records management and tag information. */
	/* TODO -- This is only a temporary solution. */
	if (index.column() == (MAX_COLNUM + TST_TRAC_NEG_COL)) {
		return tstData(index, role);
	} else if (index.column() == (MAX_COLNUM + RECMGMT_NEG_COL)) {
		return recMgmtData(index, role);
	} else if (index.column() == (MAX_COLNUM + TAGS_NEG_COL)) {
		return tagsData(index, role);
	} else if (index.column() == (MAX_COLNUM + NOTE_NEG_COL)) {
		return notesData(index, role);
	}

	switch (role) {
	case Qt::DisplayRole:
		switch (index.column()) {
		case DELIVERY_COL:
		case ACCEPT_COL:
			/* Convert date on display. */
			return dateTimeStrFromDbFormat(
			    _data(index, role).toString(),
			    Utility::dateTimeDisplayFormat);
			break;
		case PERSDELIV_COL: /* 'personal delivery' */
		case READLOC_COL: /* 'read locally' */
		case PROCSNG_COL: /* 'process status' */
		case ATTDOWN_COL: /* 'is downloaded' */
			/* Hide text. */
			return QVariant();
			break;
		default:
			return _data(index, role);
			break;
		}
		break;

	case Qt::DecorationRole:
		switch (index.column()) {
		case PERSDELIV_COL:
			/* Show icon for 'personal delivery'. */
			if (_data(index).toBool()) {
				return inconContainer.icon(IconContainer::ICON_HAND);
			} else {
				return QVariant(); /* No icon. */
			}
			break;
		case READLOC_COL:
			/* Show icon for 'read locally'. */
			if (_data(index).toBool()) {
				return inconContainer.icon(IconContainer::ICON_BALL_GREY);
			} else {
				return inconContainer.icon(IconContainer::ICON_BALL_GREEN);
			}
			break;
		case PROCSNG_COL:
			/* Show icon for 'process status'. */
			switch (_data(index).toInt()) {
			case MessageDb::UNSETTLED:
				return inconContainer.icon(IconContainer::ICON_BALL_RED);
				break;
			case MessageDb::IN_PROGRESS:
				return inconContainer.icon(IconContainer::ICON_BALL_YELLOW);
				break;
			case MessageDb::SETTLED:
				return inconContainer.icon(IconContainer::ICON_BALL_GREY);
				break;
			default:
				Q_ASSERT(0);
				break;
			}
			return QVariant();
			break;
		case ATTDOWN_COL:
			/* Show icon for 'is downloaded'. */
			if (_data(index).toBool()) {
				return inconContainer.icon(IconContainer::ICON_ATTACHMENT);
			} else {
				return QVariant(); /* No icon. */
			}
			break;
		default:
			return _data(index, role);
			break;
		}
		break;

	case Qt::ToolTipRole:
		if (index.column() == MSGSTAT_COL) {
			/* Message state in sent messages. */
			return Isds::Description::descrDmState(
			    Isds::variant2DmState(_data(index, Qt::DisplayRole)));
		}

		if ((m_msgDltNotifyAheadDays > 0) && m_msgDltNotifyColour.isValid()) {
			int rDays = MessageDb::daysRemainingSinceAcceptance(
			    dateTimeFromDbFormat(
			        _data(index.row(), ACCEPT_COL).toString()));
			if ((rDays > 0) && (rDays <= m_msgDltNotifyAheadDays)) {
				return tr(
				    "The message will be deleted from the ISDS in %n day(s).",
				    "", rDays);
			} else if (rDays == 0) {
				return tr("The message has already been deleted from the ISDS.");
			}
		}

		return QVariant();
		break;

	case Qt::FontRole:
		if (m_type == RCVD_MODEL) {
			/* In received messages. */
			if (!_data(index.sibling(index.row(),
			        READLOC_COL)).toBool()) {
				/* Unread messages are shown bold. */
				QFont boldFont;
				boldFont.setBold(true);
				return boldFont;
			}
		}
		return _data(index, role);
		break;

	case Qt::ForegroundRole:
		{
			if ((m_msgDltNotifyAheadDays > 0) && m_msgDltNotifyColour.isValid()) {
				int rDays = MessageDb::daysRemainingSinceAcceptance(
				    dateTimeFromDbFormat(
				        _data(index.row(), ACCEPT_COL).toString()));
				if ((rDays > 0) && (rDays <= m_msgDltNotifyAheadDays)) {
					return m_msgDltNotifyColour;
				}
			}

			if (m_type == RCVD_MODEL) {
				enum Isds::Type::DmState state =
				    Isds::variant2DmState(
				        _data(index.sibling(index.row(),
				            MSGSTAT_COL), Qt::DisplayRole));
				if (unacceptedState(state)) {
					return QColor(Qt::darkGray);
				}
			}
		}
		return QVariant();
		break;

	case Qt::AccessibleTextRole:
		switch (index.column()) {
		case DMID_COL:
			return tr("message identifier") + QLatin1String(" ") +
			    _data(index, Qt::DisplayRole).toString();
			break;
		case READLOC_COL:
			return headerData(index.column(), Qt::Horizontal).toString() +
			    QStringLiteral(" ") +
			    _data(index, Qt::DisplayRole).toString() +
			    QStringLiteral(" ") +
			    Isds::Description::descrDmState(
			        Isds::variant2DmState(_data(index, Qt::DisplayRole)));
			break;
		default:
			/* Continue with code. */
			break;
		}
		switch (index.column()) {
		case PERSDELIV_COL: /* 'personal delivery' */
			if (_data(index).toBool()) {
				return tr("personal delivery");
			} else {
				return tr("not a personal delivery");
			}
			break;
		case DELIVERY_COL:
		case ACCEPT_COL:
			return headerData(index.column(), Qt::Horizontal).toString() +
			    QLatin1String(" ") +
			    dateTimeStrFromDbFormat(
			        _data(index, Qt::DisplayRole).toString(),
			        Utility::dateTimeDisplayFormat);
			break;
		case READLOC_COL: /* 'read locally' */
			if (_data(index).toBool()) {
				return tr("marked as read");
			} else {
				return tr("marked as unread");
			}
			break;
		case ATTDOWN_COL: /* 'is downloaded' */
			if (_data(index).toBool()) {
				return tr("attachments downloaded");
			} else {
				return tr("attachments not downloaded");
			}
			break;
		case PROCSNG_COL: /* 'process status' */
			{
				QString headerPref(headerData(index.column(),
				     Qt::Horizontal, Qt::ToolTipRole).toString());
				headerPref += QLatin1String(" ");

				switch (_data(index).toInt()) {
				case MessageDb::UNSETTLED:
					return headerPref + tr("unsettled");
					break;
				case MessageDb::IN_PROGRESS:
					return headerPref + tr("in progress");
					break;
				case MessageDb::SETTLED:
					return headerPref + tr("settled");
					break;
				default:
					Q_ASSERT(0);
					break;
				}
			}
			return QVariant();
			break;
		default:
			return headerData(index.column(), Qt::Horizontal).toString() +
			    QLatin1String(" ") +
			    _data(index, Qt::DisplayRole).toString();
			break;
		}
		break;

	case ROLE_PLAIN_DISPLAY:
		return _data(index, Qt::DisplayRole);
		break;

	case ROLE_PROXYSORT:
		switch (index.column()) {
		case PERSDELIV_COL:
		case READLOC_COL:
		case ATTDOWN_COL:
			return ModelHelper::sortRank(
			    _data(index, Qt::DisplayRole).toBool() ? 1 : 0,
			    getDmId(index));
			break;
		case PROCSNG_COL:
			return ModelHelper::sortRank(
			    _data(index, Qt::DisplayRole).toInt(),
			    getDmId(index));
			break;
		case ANNOT_COL: /* Ignore case for sorting. */
		case SENDER_COL:
		case RECIP_COL:
			return _data(index,
			    Qt::DisplayRole).toString().toLower();
			break;
		case DELIVERY_COL:
		case ACCEPT_COL:
			return ModelHelper::sortRank(
			    _data(index, Qt::DisplayRole).toDateTime(),
			    getDmId(index));
			break;
		default:
			return _data(index, Qt::DisplayRole);
			break;
		}
		break;

	default:
		return _data(index, role);
		break;
	}
}

QVariant DbMsgsTblModel::headerData(int section, Qt::Orientation orientation,
    int role) const
{
	/* Draw additional. */
	if (section >= BASIC_COLNUM) {
		return _headerData(section, orientation, role);
	}

	switch (role) {
	case Qt::DisplayRole:
		switch (section) {
		case PERSDELIV_COL: /* 'personal delivery' */
		case READLOC_COL: /* 'read locally' */
		case ATTDOWN_COL: /* 'is downloaded' */
		case PROCSNG_COL: /* 'process status' */
			/* Hide text. */
			return QVariant();
			break;
		default:
			return _headerData(section, orientation, role);
			break;
		}
		break;

	case Qt::DecorationRole:
		switch (section) {
		case PERSDELIV_COL: /* 'personal delivery' */
			return inconContainer.icon(IconContainer::ICON_HAND);
			break;
		case READLOC_COL: /* 'read locally' */
			return inconContainer.icon(IconContainer::ICON_READCOL);
			break;
		case ATTDOWN_COL: /* 'is downloaded' */
			return inconContainer.icon(IconContainer::ICON_ATTACHMENT);
			break;
		case PROCSNG_COL: /* 'process status' */
			return inconContainer.icon(IconContainer::ICON_FLAG);
			break;
		default:
			return _headerData(section, orientation, role);
			break;
		}
		break;

	case Qt::ToolTipRole:
		switch (section) {
		case PERSDELIV_COL: /* 'personal delivery' */
		case READLOC_COL: /* 'read locally' */
		case ATTDOWN_COL: /* 'is downloaded' */
		case PROCSNG_COL: /* 'process status' */
			/* Tool tip. */
			return _headerData(section, orientation, Qt::DisplayRole);
			break;
		default:
			return QVariant();
			break;
		}
		break;

	default:
		return _headerData(section, orientation, role);
		break;
	}
}

Qt::ItemFlags DbMsgsTblModel::flags(const QModelIndex &index) const
{
	Qt::ItemFlags defaultFlags = TblModel::flags(index);

	if (index.isValid()) {
		defaultFlags |= Qt::ItemIsDragEnabled;
	}

	return defaultFlags;
}

QStringList DbMsgsTblModel::mimeTypes(void) const
{
	return QStringList(QStringLiteral("text/uri-list"));
}

/*!
 * @brief Converts list of absolute file names to list of URLs.
 *
 * @param[in] fileNames List of absolute file names.
 * @return List of URLs or empty list on error.
 */
static
QList<QUrl> fileUrls(const QStringList &fileNames)
{
	QList<QUrl> uriList;

	for (const QString &fileName : fileNames) {
		uriList.append(QUrl::fromLocalFile(fileName));
	}

	return uriList;
}

QMimeData *DbMsgsTblModel::mimeData(const QModelIndexList &indexes) const
{
	if (Q_UNLIKELY(!m_acntIdDb.isValid())) {
		logErrorNL("%s",
		    "Ignoring drag because of invalid account identifier.");
		return Q_NULLPTR;
	}

	QModelIndexList lineIndexes = TblModel::sortedUniqueLineIndexes(indexes,
	    DMID_COL);
	if (Q_UNLIKELY(lineIndexes.isEmpty())) {
		return Q_NULLPTR;
	}

	const QString tmpDirPath = createTemporarySubdir(TMP_DIR_NAME);
	if (Q_UNLIKELY(tmpDirPath.isEmpty())) {
		return Q_NULLPTR;
	}

	/* Collect message identifiers. */
	QList<MsgId> msgIdsDownloaded;
	QStringList filePaths;
	{
		AcntIdToMsgIdListHash acntMsgIds; /* Have not been downloaded. */
		for (const QModelIndex &index : lineIndexes) {
			const bool downloaded =
			    _data(index.row(), ATTDOWN_COL, Qt::DisplayRole).toBool();
			MsgId msgId(
			    _data(index.row(), DMID_COL, Qt::DisplayRole).toLongLong(),
			    dateTimeFromDbFormat(_data(index.row(), DELIVERY_COL, Qt::DisplayRole).toString()));
			if (downloaded) {
				msgIdsDownloaded.append(msgId);
			} else {
				acntMsgIds[m_acntIdDb].append(msgId);
			}
		}

		if (Q_UNLIKELY(!acntMsgIds.isEmpty())) {
			/* Abort if some not downloaded. */
			Q_EMIT missingCompleteMessages(acntMsgIds);
			goto fail;
		}
	}

	if (Q_UNLIKELY(msgIdsDownloaded.isEmpty())) {
		goto fail;
	}

	/* Generate temporary files. */
	for (const MsgId &msgId : msgIdsDownloaded) {
		MessageDbSet *dbSet = m_acntIdDb.messageDbSet();
		if (Q_UNLIKELY(Q_NULLPTR == dbSet)) {
			Q_ASSERT(0);
			continue;
		}

		MessageDb *messageDb = dbSet->accessMessageDb(
		    msgId.deliveryTime(), false);
		if (Q_UNLIKELY(Q_NULLPTR == messageDb)) {
			Q_ASSERT(0);
			continue;
		}

		const QString path = tmpDirPath % QStringLiteral("/")
		    % QString("%1-%2.zfo")
		        .arg(Exports::dmTypePrefix(messageDb, msgId.dmId()))
		        .arg(msgId.dmId());

		const QByteArray data = messageDb->getCompleteMessageRaw(msgId.dmId());
		if (Q_UNLIKELY(data.isEmpty())) {
			logErrorNL("Cannot read data of message '%" PRId64 "'.",
			    UGLY_QINT64_CAST msgId.dmId());
			goto fail;
		}

		if (WF_SUCCESS == writeFile(path, data, true)) {
			filePaths.append(path);
		} else {
			logErrorNL("Cannot write file '%s'.",
			    path.toUtf8().constData());
			goto fail;
		}
	}

	if (Q_UNLIKELY(filePaths.isEmpty())) {
		logErrorNL("%s", "Could not write temporary files.");
		return Q_NULLPTR;
	}

	{
		QMimeData *mimeData = new (::std::nothrow) QMimeData();
		if (Q_UNLIKELY(Q_NULLPTR == mimeData)) {
			goto fail;
		}
		{
			QList<QUrl> urlList = fileUrls(filePaths);
			mimeData->setUrls(urlList);
			return mimeData;
		}
	}

fail:
	/* Delete temporary files. */
	QDir(tmpDirPath).removeRecursively();
	return Q_NULLPTR;
}

void DbMsgsTblModel::assignData(const AcntIdDb &acntIdDb,
    const QList<MessageDb::RcvdEntry> &entryList, int appendedColsNum)
{
	if (Q_UNLIKELY(m_columnCount != MAX_COLNUM)) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(appendedColsNum < 0)) {
		Q_ASSERT(0);
		return;
	}

	removeRows(0, rowCount());

	/* Set column count if the model is empty. */
	if (rowCount() == 0) {
		beginResetModel();
		m_acntIdDb = acntIdDb;
		m_type = RCVD_MODEL;
		//m_columnCount = MAX_COLNUM;
		endResetModel();
	} else {
		if (Q_UNLIKELY(m_acntIdDb != acntIdDb)) {
			Q_ASSERT(0);
			return;
		}
		if (Q_UNLIKELY(m_type != RCVD_MODEL)) {
			Q_ASSERT(0);
			return;
		}
	}

	if (entryList.isEmpty()) {
		/* Don't do anything. */
		return;
	}

	beginInsertRows(QModelIndex(), rowCount(),
	    rowCount() + entryList.size() - 1);

	for (const MessageDb::RcvdEntry &entry : entryList) {

		reserveSpace();

		QVector<QVariant> row(m_columnCount);

		row[DMID_COL] = entry.dmId;
		row[PERSDELIV_COL] = entry.dmPersonalDelivery;
		row[ANNOT_COL] = entry.dmAnnotation;
		row[SENDER_COL] = entry.dmSender;
		row[DELIVERY_COL] = entry.dmDeliveryTime;
		row[ACCEPT_COL] = entry.dmAcceptanceTime;
		row[READLOC_COL] = entry.readLocally;
		row[MSGSTAT_COL] = entry.dmMessageStatus;
		row[ATTDOWN_COL] = entry.isDownloaded;
		row[PROCSNG_COL] = entry.processStatus;

		m_data[m_rowCount++] = macroStdMove(row);
	}

	endInsertRows();
}

void DbMsgsTblModel::assignData(const AcntIdDb &acntIdDb,
    const QList<MessageDb::SntEntry> &entryList, int appendedColsNum)
{
	if (Q_UNLIKELY(m_columnCount != MAX_COLNUM)) {
		Q_ASSERT(0);
		return;
	}
	if (Q_UNLIKELY(appendedColsNum < 0)) {
		Q_ASSERT(0);
		return;
	}

	removeRows(0, rowCount());

	/* Set column count if the model is empty. */
	if (rowCount() == 0) {
		beginResetModel();
		m_acntIdDb = acntIdDb;
		m_type = SNT_MODEL;
		//m_columnCount = MAX_COLNUM;
		endResetModel();
	} else {
		if (Q_UNLIKELY(m_acntIdDb != acntIdDb)) {
			Q_ASSERT(0);
			return;
		}
		if (Q_UNLIKELY(m_type != SNT_MODEL)) {
			Q_ASSERT(0);
			return;
		}
	}

	if (entryList.isEmpty()) {
		/* Don't do anything. */
		return;
	}

	beginInsertRows(QModelIndex(), rowCount(),
	    rowCount() + entryList.size() - 1);

	for (const MessageDb::SntEntry &entry : entryList) {

		reserveSpace();

		QVector<QVariant> row(m_columnCount);

		row[DMID_COL] = entry.dmId;
		row[ANNOT_COL] = entry.dmAnnotation;
		row[RECIP_COL] = entry.dmRecipient;
		row[DELIVERY_COL] = entry.dmDeliveryTime;
		row[ACCEPT_COL] = entry.dmAcceptanceTime;
		row[MSGSTAT_COL] = entry.dmMessageStatus;
		row[ATTDOWN_COL] = entry.isDownloaded;

		m_data[m_rowCount++] = macroStdMove(row);
	}

	endInsertRows();
}

bool DbMsgsTblModel::setType(enum Type type)
{
	m_type = type;

	return true;
}

enum DbMsgsTblModel::Type DbMsgsTblModel::type(void) const
{
	return m_type;
}

static
void appendHeaderColumns(DbMsgsTblModel *model, int dfltHdrSize,
    const QList<DbMsgsTblModel::AppendedCol> &appendedCols)
{
	if ((model == Q_NULLPTR) || (dfltHdrSize < 0)) {
		Q_ASSERT(0);
		return;
	}

	for (int i = 0; i < appendedCols.size(); ++i) {
		/* Description. */
		model->setHeaderData(dfltHdrSize + i, Qt::Horizontal,
		    appendedCols.at(i).display, Qt::DisplayRole);
		model->setHeaderData(dfltHdrSize + i, Qt::Horizontal,
		    appendedCols.at(i).decoration, Qt::DecorationRole);
		model->setHeaderData(dfltHdrSize + i, Qt::Horizontal,
		    appendedCols.at(i).toolTip, Qt::ToolTipRole);
		/* Data type. */
		model->setHeaderData(dfltHdrSize + i, Qt::Horizontal,
		    DB_APPENDED_VARIANT, DbMsgsTblModel::ROLE_DB_ENTRY_TYPE);
	}
}

bool DbMsgsTblModel::setHeader(const QList<AppendedCol> &appendedCols)
{
	/* Sets description and data type. */

	setHeaderData(DMID_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[0]).desc,
	    Qt::DisplayRole);
	setHeaderData(DMID_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[0]).type,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(PERSDELIV_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[1]).desc,
	    Qt::DisplayRole);
	setHeaderData(PERSDELIV_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[1]).type,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(ANNOT_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[2]).desc,
	    Qt::DisplayRole);
	setHeaderData(ANNOT_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[2]).type,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(SENDER_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[3]).desc,
	    Qt::DisplayRole);
	setHeaderData(SENDER_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[3]).type,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(RECIP_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::sntItemIds[2]).desc,
	    Qt::DisplayRole);
	setHeaderData(RECIP_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::sntItemIds[2]).type,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(DELIVERY_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[4]).desc,
	    Qt::DisplayRole);
	setHeaderData(DELIVERY_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[4]).type,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(ACCEPT_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[5]).desc,
	    Qt::DisplayRole);
	setHeaderData(ACCEPT_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::rcvdItemIds[5]).type,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(READLOC_COL, Qt::Horizontal,
	    MessageDbTables::smsgdtTbl.attrProps.value(MessageDb::rcvdItemIds[6]).desc,
	    Qt::DisplayRole);
	setHeaderData(READLOC_COL, Qt::Horizontal,
	    MessageDbTables::smsgdtTbl.attrProps.value(MessageDb::rcvdItemIds[6]).type,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(MSGSTAT_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::sntItemIds[5]).desc,
	    Qt::DisplayRole);
	setHeaderData(MSGSTAT_COL, Qt::Horizontal,
	    MessageDbTables::msgsTbl.attrProps.value(MessageDb::sntItemIds[5]).type,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(ATTDOWN_COL, Qt::Horizontal,
	    tr("Attachments downloaded"), Qt::DisplayRole);
	setHeaderData(ATTDOWN_COL, Qt::Horizontal, DB_BOOLEAN,
	    ROLE_DB_ENTRY_TYPE);

	setHeaderData(PROCSNG_COL, Qt::Horizontal, tr("Processing state"),
	    Qt::DisplayRole);
	setHeaderData(PROCSNG_COL, Qt::Horizontal, DB_INTEGER,
	    ROLE_DB_ENTRY_TYPE);

	appendHeaderColumns(this, BASIC_COLNUM, appendedCols);

	return true;
}

bool DbMsgsTblModel::remove(qint64 dmId)
{
	for (int row = 0; row < rowCount(); ++row) {
		if (_data(row, DMID_COL, Qt::DisplayRole).toLongLong() == dmId) {
			return removeRows(row, 1);
		}
	}

	return false;
}

bool DbMsgsTblModel::overrideRead(qint64 dmId, bool forceRead)
{
	if (RCVD_MODEL != m_type) {
		/* Not a read messages model. */
		return false;
	}

	for (int row = 0; row < rowCount(); ++row) {
		if (_data(row, DMID_COL, Qt::DisplayRole).toLongLong() == dmId) {
			m_data[row][READLOC_COL] = QVariant(forceRead);

			emit dataChanged(TblModel::index(row, 0),
			    TblModel::index(row, columnCount() - 1));
			return true;
		}
	}

	return false;
}

/*!
 * @brief Converts list of message identifiers into a set of message IDs.
 *
 * @param[in] msgIds List of message identifiers.
 * @return Set of message IDs.
 */
static inline
QSet<qint64> toDmIdSet(const QList<MsgId> &msgIds)
{
	QSet<qint64> dmIds;
	for (const MsgId &msgId : msgIds) {
		if (Q_UNLIKELY(msgId.dmId() < 0)) {
			Q_ASSERT(0);
			continue;
		}
		dmIds.insert(msgId.dmId());
	}
	return dmIds;
}

bool DbMsgsTblModel::overrideRead(const QList<MsgId> &msgIds, bool forceRead)
{
	if (RCVD_MODEL != m_type) {
		/* Not a read messages model. */
		return false;
	}

	bool found = false;

	/* Construct a set of message IDs. */
	QSet<qint64> dmIdSet = toDmIdSet(msgIds);
	QSet<qint64>::iterator it;

	for (int row = 0; row < rowCount(); ++row) {
		if (Q_UNLIKELY(dmIdSet.isEmpty())) {
			return found;
		}

		it = dmIdSet.find(
		    _data(row, DMID_COL, Qt::DisplayRole).toLongLong());
		if (it != dmIdSet.end()) {
			found = true;

			m_data[row][READLOC_COL] = QVariant(forceRead);

			emit dataChanged(TblModel::index(row, 0),
			    TblModel::index(row, columnCount() - 1));

#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
			dmIdSet.erase(it);
#else /* < Qt-5.7.0 */
			dmIdSet.remove(*it);
#endif /* >= Qt-5.7.0 */
		}
	}

	return found;
}

bool DbMsgsTblModel::overrideDownloaded(qint64 dmId, bool forceDownloaded)
{
	if (RCVD_MODEL != m_type) {
		/* Not a read messages model. */
		return false;
	}

	for (int row = 0; row < rowCount(); ++row) {
		if (_data(row, DMID_COL, Qt::DisplayRole).toLongLong() == dmId) {
			m_data[row][ATTDOWN_COL] = QVariant(forceDownloaded);

			emit dataChanged(TblModel::index(row, 0),
			    TblModel::index(row, columnCount() - 1));
			return true;
		}
	}

	return false;
}

bool DbMsgsTblModel::overrideProcessing(qint64 dmId,
    enum MessageDb::MessageProcessState forceState)
{
	if (RCVD_MODEL != m_type) {
		/* Not a read messages model. */
		return false;
	}

	for (int row = 0; row < rowCount(); ++row) {
		if (_data(row, DMID_COL, Qt::DisplayRole).toLongLong() == dmId) {
			m_data[row][PROCSNG_COL] = QVariant(forceState);

			emit dataChanged(TblModel::index(row, 0),
			    TblModel::index(row, columnCount() - 1));
			return true;
		}
	}

	return false;
}

bool DbMsgsTblModel::overrideProcessing(const QList<MsgId> &msgIds,
    enum MessageDb::MessageProcessState forceState)
{
	if (RCVD_MODEL != m_type) {
		/* Not a read messages model. */
		return false;
	}

	bool found = false;

	/* Construct a set of message IDs. */
	QSet<qint64> dmIdSet = toDmIdSet(msgIds);
	QSet<qint64>::iterator it;

	for (int row = 0; row < rowCount(); ++row) {
		if (Q_UNLIKELY(dmIdSet.isEmpty())) {
			return found;
		}

		it = dmIdSet.find(
		    _data(row, DMID_COL, Qt::DisplayRole).toLongLong());
		if (it != dmIdSet.end()) {
			found = true;

			m_data[row][PROCSNG_COL] = QVariant(forceState);

			emit dataChanged(TblModel::index(row, 0),
			    TblModel::index(row, columnCount() - 1));

#if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
			dmIdSet.erase(it);
#else /* < Qt-5.7.0 */
			dmIdSet.remove(*it);
#endif /* >= Qt-5.7.0 */
		}
	}

	return found;
}

Json::MsgId1List DbMsgsTblModel::msgIdList(bool testEnv) const
{
	Json::MsgId1List msgIds;
	for (int row = 0; row < rowCount(); ++row) {
		qint64 dmId = TblModel::index(row, 0).data().toLongLong();
		const Json::MsgId1 msgId(
		    testEnv ? Isds::Type::BOOL_TRUE : Isds::Type::BOOL_FALSE,
		    dmId);
		msgIds.append(msgId);
	}
	return msgIds;
}

bool DbMsgsTblModel::setTagAssignments(bool testEnv, int col,
    const Json::TagAssignmentHash &assignments)
{
	/* Check indexes into column. */
	if (col >= 0) {
		if (col > columnCount()) {
			return false;
		}
	} else {
		col += columnCount();
		if (col < 0) {
			return false;
		}
	}

	for (int row = 0; row < rowCount(); ++row) {
		qint64 dmId = TblModel::index(row, 0).data().toLongLong();
		const Json::MsgId1 msgId(
		    testEnv ? Isds::Type::BOOL_TRUE : Isds::Type::BOOL_FALSE,
		    dmId);
		TagItemList tagList = assignments.value(msgId, Json::TagEntryList());
		tagList.sortNames();
		m_data[row][col] = QVariant::fromValue(tagList);
	}

	emit dataChanged(TblModel::index(0, col),
	    TblModel::index(rowCount() - 1, col));

	return true;
}

void DbMsgsTblModel::updateTag(const Json::TagEntry &entry)
{
	if (Q_UNLIKELY(!entry.isValid())) {
		return;
	}

	int col = MAX_COLNUM + TAGS_NEG_COL;

	for (int row = 0; row < rowCount(); ++row) {
		QVariant &value = m_data[row][col];
		if (!value.canConvert<TagItemList>()) {
			continue;
		}
		TagItemList tagList(qvariant_cast<TagItemList>(value));
		if (tagList.updateTag(entry)) {
			/* Sort the modified list and store back into model. */
			tagList.sortNames();
			value = QVariant::fromValue(tagList);
			emit dataChanged(TblModel::index(row, col),
			    TblModel::index(row, col));
		}
	}
}

void DbMsgsTblModel::deleteTags(const Json::Int64StringList &ids)
{
	if (Q_UNLIKELY(ids.isEmpty())) {
		return;
	}

	int col = MAX_COLNUM + TAGS_NEG_COL;

	for (int row = 0; row < rowCount(); ++row) {
		QVariant &value = m_data[row][col];
		if (!value.canConvert<TagItemList>()) {
			continue;
		}
		TagItemList tagList(qvariant_cast<TagItemList>(value));
		if (tagList.deleteTags(ids)) {
			/* No need to sort anything when deleted. */
			value = QVariant::fromValue(tagList);
			emit dataChanged(TblModel::index(row, col),
			    TblModel::index(row, col));
		}
	}
}

void DbMsgsTblModel::updateTagAssignment(qint64 dmId,
    const Json::TagEntryList &entries)
{
	if (Q_UNLIKELY(dmId < 0)) {
		return;
	}

	int tagCol = MAX_COLNUM + TAGS_NEG_COL;

	for (int row = 0; row < rowCount(); ++row) {
		if (_data(row, DMID_COL, Qt::DisplayRole).toLongLong() != dmId) {
			continue;
		}

		QVariant &value = m_data[row][tagCol];
		if (!value.canConvert<TagItemList>()) {
			continue;
		}
		TagItemList tagList = entries;
		/* Sort the new list and store back into model. */
		tagList.sortNames();
		value = QVariant::fromValue(tagList);
		emit dataChanged(TblModel::index(row, tagCol),
		    TblModel::index(row, tagCol));
	}
}

bool DbMsgsTblModel::setNoteAssignments(bool testEnv, int col,
    const Json::NoteAssignmentHash &assignments)
{
	/* Check indexes into column. */
	if (col >= 0) {
		if (col > columnCount()) {
			return false;
		}
	} else {
		col += columnCount();
		if (col < 0) {
			return false;
		}
	}

	for (int row = 0; row < rowCount(); ++row) {
		qint64 dmId = TblModel::index(row, 0).data().toLongLong();
		const Json::MsgId1 msgId(
		    testEnv ? Isds::Type::BOOL_TRUE : Isds::Type::BOOL_FALSE,
		    dmId);
		m_data[row][col] = QVariant::fromValue(assignments.value(msgId, Json::NoteEntryList()));
	}

	emit dataChanged(TblModel::index(0, col),
	    TblModel::index(rowCount() - 1, col));

	return true;
}

void DbMsgsTblModel::updateNote(const Json::NoteEntry &entry)
{
	if (Q_UNLIKELY(!entry.isValid())) {
		return;
	}

	int col = MAX_COLNUM + NOTE_NEG_COL;

	for (int row = 0; row < rowCount(); ++row) {
		QVariant &value = m_data[row][col];
		if (!value.canConvert<Json::NoteEntryList>()) {
			continue;
		}
		Json::NoteEntryList noteList = qvariant_cast<Json::NoteEntryList>(value);
		for (int i = 0; i < noteList.size(); ++i) {
			if (noteList.at(i).id() == entry.id()) {
				noteList[i] = entry;
				value = QVariant::fromValue(noteList);
				emit dataChanged(TblModel::index(row, col),
				    TblModel::index(row, col));
				break;
			}
		}
	}
}

void DbMsgsTblModel::deleteNotes(const Json::Int64StringList &ids)
{
	if (Q_UNLIKELY(ids.isEmpty())) {
		return;
	}

	int col = MAX_COLNUM + NOTE_NEG_COL;

	for (int row = 0; row < rowCount(); ++row) {
		QVariant &value = m_data[row][col];
		if (!value.canConvert<Json::NoteEntryList>()) {
			continue;
		}
		Json::NoteEntryList noteList = qvariant_cast<Json::NoteEntryList>(value);

		bool removed = false;
		for (const qint64 id : ids) {
			for (int i = 0; i < noteList.size(); ++i) {
				if (noteList.at(i).id() == id) {
					noteList.removeAt(i);
					removed = true;
					break;
				}
			}
		}

		if (removed) {
			value = QVariant::fromValue(noteList);
			emit dataChanged(TblModel::index(row, col),
			    TblModel::index(row, col));
		}
	}
}

void DbMsgsTblModel::updateNoteAssignment(qint64 dmId,
    const Json::NoteEntryList &entries)
{
	if (Q_UNLIKELY(dmId < 0)) {
		return;
	}

	int tagCol = MAX_COLNUM + NOTE_NEG_COL;

	for (int row = 0; row < rowCount(); ++row) {
		if (_data(row, DMID_COL, Qt::DisplayRole).toLongLong() != dmId) {
			continue;
		}

		QVariant &value = m_data[row][tagCol];
		if (!value.canConvert<Json::NoteEntryList>()) {
			continue;
		}
		Json::NoteEntryList noteList = entries;
		/* Store back into model. */
		value = QVariant::fromValue(noteList);
		emit dataChanged(TblModel::index(row, tagCol),
		    TblModel::index(row, tagCol));
	}
}

bool DbMsgsTblModel::setTimestampTrackings(bool testEnv, int col,
    const TstValidityHash &tstValues)
{
	/* Check indexes into column. */
	if (col >= 0) {
		if (col > columnCount()) {
			return false;
		}
	} else {
		col += columnCount();
		if (col < 0) {
			return false;
		}
	}

	for (int row = 0; row < rowCount(); ++row) {
		qint64 dmId = TblModel::index(row, 0).data().toLongLong();
		const Json::MsgId1 msgId(
		    testEnv ? Isds::Type::BOOL_TRUE : Isds::Type::BOOL_FALSE,
		    dmId);
		TstValidity value = tstValues.value(msgId, TstValidity());
		m_data[row][col] = QVariant::fromValue(value);
	}

	emit dataChanged(TblModel::index(0, col),
	    TblModel::index(rowCount() - 1, col));

	return true;
}

void DbMsgsTblModel::updateTimestampTracking(qint64 dmId,
    const TstValidity &tstValue)
{
	if (Q_UNLIKELY(dmId < 0)) {
		return;
	}

	int tstCol = MAX_COLNUM + TST_TRAC_NEG_COL;

	for (int row = 0; row < rowCount(); ++row) {
		if (_data(row, DMID_COL, Qt::DisplayRole).toLongLong() != dmId) {
			continue;
		}

		QVariant &value = m_data[row][tstCol];
		if (!value.canConvert<TstValidity>()) {
			continue;
		}

		/* Store into model. */
		value = QVariant::fromValue(tstValue);
		emit dataChanged(TblModel::index(row, tstCol),
		    TblModel::index(row, tstCol));
	}
}

bool DbMsgsTblModel::setRecordsManagementIcon(void)
{
	if (Q_NULLPTR == GlobInstcs::recMgmtDbPtr) {
		m_rmIco = defaultRMIcon();
		return false;
	}

	RecordsManagementDb::ServiceInfoEntry entry(
	    GlobInstcs::recMgmtDbPtr->serviceInfo());
	if (!entry.isValid() || entry.logoSvg.isEmpty()) {
		m_rmIco = defaultRMIcon();
		return false;
	}
	QPixmap pixmap16(Graphics::pixmapFromSvg(entry.logoSvg, 16));
	QPixmap pixmap32(Graphics::pixmapFromSvg(entry.logoSvg, 32));
	if (pixmap16.isNull() || pixmap32.isNull()) {
		m_rmIco = defaultRMIcon();
		return false;
	}

	QIcon ico;
	ico.addPixmap(pixmap16, QIcon::Normal, QIcon::Off);
	ico.addPixmap(pixmap32, QIcon::Normal, QIcon::Off);
	m_rmIco = macroStdMove(ico);
	return true;
}

void DbMsgsTblModel::setMsgDeletionNotifyAheadDays(int days)
{
	if (days < 0) {
		days = 0;
	}
	if (m_msgDltNotifyAheadDays == days) {
		return;
	}
	m_msgDltNotifyAheadDays = days;
	QVector<int> roles(2);
	roles[0] = Qt::ToolTipRole;
	roles[1] = Qt::ForegroundRole;
	emit dataChanged(TblModel::index(0, 0),
	    TblModel::index(rowCount() - 1, columnCount() - 1), roles);
}

void DbMsgsTblModel::setMsgDeletionNotifyAheadColour(const QColor &colour)
{
	if (m_msgDltNotifyColour == colour) {
		return;
	}
	m_msgDltNotifyColour = colour.isValid() ? colour : QColor();
	QVector<int> roles(1);
	roles[0] = Qt::ForegroundRole;
	emit dataChanged(TblModel::index(0, 0),
	    TblModel::index(rowCount() - 1, columnCount() - 1), roles);
}

bool DbMsgsTblModel::fillRecordsManagementColumn(int col)
{
	if (Q_NULLPTR == GlobInstcs::recMgmtDbPtr) {
		return false;
	}

	/* Check indexes into column. */
	if (col >= 0) {
		if (col > columnCount()) {
			return false;
		}
	} else {
		col += columnCount();
		if (col < 0) {
			return false;
		}
	}

	for (int row = 0; row < rowCount(); ++row) {
		qint64 dmId = TblModel::index(row, 0).data().toLongLong();
		m_data[row][col] =
		    GlobInstcs::recMgmtDbPtr->storedMsgLocations(dmId);
	}

	emit dataChanged(TblModel::index(0, col),
	    TblModel::index(rowCount() - 1, col));

	return true;
}

bool DbMsgsTblModel::updateRecordsManagementColumn(qint64 dmId,
    const QStringList &locations, int col)
{
	/* Check indexes into column. */
	if (col >= 0) {
		if (col > columnCount()) {
			return false;
		}
	} else {
		col += columnCount();
		if (col < 0) {
			return false;
		}
	}

	for (int row = 0; row < rowCount(); ++row) {
		const qint64 modelDmId = TblModel::index(row, 0).data().toLongLong();
		if (modelDmId == dmId) {
			m_data[row][col] = locations;
			emit dataChanged(TblModel::index(row, col),
			    TblModel::index(row, col));
		}
	}

	return true;
}

QVariant DbMsgsTblModel::notesData(const QModelIndex &index, int role) const
{
	QVariant rawData(_data(index, Qt::DisplayRole));
	if (Q_UNLIKELY(!rawData.canConvert<Json::NoteEntryList>())) {
		/*
		 * Cannot call Q_ASSERT(0) here as this method
		 * may get called on invalid data occasionally.
		 */
		logWarningNL("%s",
		    "Did not get notes data.");
		return QVariant();
	}

	const Json::NoteEntryList noteList = qvariant_cast<Json::NoteEntryList>(rawData);

	switch (role) {
	case Qt::DisplayRole:
		return QVariant();
		break;
	case Qt::DecorationRole:
		/* Show icon for 'message assigned'. */
		if (!noteList.isEmpty()) {
			return inconContainer.icon(IconContainer::ICON_APP_PENCIL);
		} else {
			return QVariant(); /* No icon. */
		}
		break;
	case Qt::ToolTipRole:
		if (!noteList.isEmpty()) {
			QString notes;
			for (int i = 0; i < noteList.size(); ++i) {
				if (i == 0) {
					notes = noteList.at(i).content();
				} else {
					notes += "\n\n" + noteList.at(i).content();
				}
			}
			return notes;
		} else {
			return QVariant();
		}
		break;
	case ROLE_PROXYSORT:
		return ModelHelper::sortRank((!noteList.isEmpty()) ? 1 : 0,
		    getDmId(index));
		break;
	default:
		return QVariant();
		break;
	}
}

QVariant DbMsgsTblModel::recMgmtData(const QModelIndex &index, int role) const
{
	QStringList locations;
	{
		QVariant rawData(_data(index, Qt::DisplayRole));
		if (Q_UNLIKELY(!rawData.canConvert<QStringList>())) {
			/*
			 * Cannot call Q_ASSERT(0) here as this method
			 * may get called on invalid data occasionally.
			 */
			logWarningNL("%s",
			    "Did not get records management data.");
			return QVariant();
		}
		locations = rawData.toStringList();
	}

	switch (role) {
	case Qt::DecorationRole:
		return locations.isEmpty() ? QVariant() : m_rmIco;
		break;
	case Qt::ToolTipRole:
		return locations.join(QLatin1String("\n"));
		break;
	case Qt::AccessibleTextRole:
		if (!locations.isEmpty()) {
			return headerData(index.column(), Qt::Horizontal, Qt::ToolTipRole).toString() +
			    QLatin1String(": ") +
			    locations.join(QLatin1String(", "));
		}
		return QVariant();
		break;
	case ROLE_PROXYSORT:
		return ModelHelper::sortRank(locations.isEmpty() ? 0 : 1,
		    getDmId(index));
		break;
	default:
		return QVariant();
		break;
	}
}

QVariant DbMsgsTblModel::tstData(const QModelIndex &index, int role) const
{
	TstValidity validity;
	{
		QVariant rawData(_data(index, Qt::DisplayRole));
		if (Q_UNLIKELY(!rawData.canConvert<TstValidity>())) {
			/*
			 * Cannot call Q_ASSERT(0) here as this method
			 * may get called on invalid data occasionally.
			 */
			logWarningNL("%s",
			    "Did not get timestamp data.");
			return QVariant();
		}
		validity = qvariant_cast<TstValidity>(rawData);
	}

	switch (role) {
	case Qt::DecorationRole:
		return validity.tstTracked
		    ? inconContainer.icon(IconContainer::ICON_CLOCK)
		    : QVariant();
		break;
	case Qt::ToolTipRole:
		return validity.tstExpiration.isValid()
		    ? tr("Message timestamp expires at %1.")
		        .arg(validity.tstExpiration.toString(Utility::dateTimeDisplayFormat))
		    : QVariant();
		break;
	case Qt::AccessibleTextRole:
		return validity.tstTracked
		    ? tr("Message timestamp is being tracked.")
		    : QVariant();
		break;
	case ROLE_PROXYSORT:
		return ModelHelper::sortRank(validity.tstTracked ? 0 : 1,
		    getDmId(index));
		break;
	default:
		return QVariant();
		break;
	}
}

QVariant DbMsgsTblModel::tagsData(const QModelIndex &index, int role) const
{
	switch (role) {
	case Qt::AccessibleTextRole:
		{
			QVariant val(_data(index, Qt::DisplayRole));
			if (Q_UNLIKELY(!val.canConvert<TagItemList>())) {
				/*
				 * Cannot call Q_ASSERT(0) here as this method
				 * may get called on invalid data occasionally.
				 */
				logWarningNL("%s", "Did not get tag data.");
				return QVariant();
			}

			const TagItemList tagList(
			    qvariant_cast<TagItemList>(val));
			if (!tagList.isEmpty()) {
				QString descr(
				    headerData(index.column(), Qt::Horizontal).toString() +
				    QLatin1String(": "));
				for (int i = 0; i < tagList.size(); ++i) {
					if (i > 0) {
						descr += QLatin1String(", ");
					}
					descr += tagList.at(i).name();
				}
				return descr;
			}
		}
		return QVariant();
		break;
	case ROLE_PROXYSORT:
		/* Sort proxy model is able to handle tags. */
		return _data(index, Qt::DisplayRole);
		break;
	default:
		/* Leave additional tags to delegates. */
		return _data(index, role);
		break;
	}
}

qint64 DbMsgsTblModel::getDmId(const QModelIndex &index) const
{
	return _data(index.sibling(index.row(), DMID_COL),
	    Qt::DisplayRole).toLongLong();
}
