/*
 * 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 <QFont>
#include <QMimeData>
#include <QRegularExpression>

#include "src/datovka_shared/log/log.h"
#include "src/gui/icon_container.h"
#include "src/io/message_db.h"
#include "src/models/accounts_model.h"
#include "src/settings/account.h"
#include "src/settings/accounts.h"

/*
 * For index navigation QModellIndex::internalId() is used. The value encodes
 * the index of the top account node (which is also the index into the array
 * of user names) and the actual node type.
 *
 * The 4 least significant bits hold the node type.
 */
#define TYPE_BITS 4
#define TYPE_MASK 0x0f

/*!
 * @brief Generates model index internal identifier.
 *
 * @param[in] numId Numeric equivalent of the account identifier.
 * @param[in] nodeType Node type.
 * @return Internal identifier.
 */
#define internalIdCreate(numId, nodeType) \
	((((quintptr) (numId)) << TYPE_BITS) | (nodeType))

/*!
 * @brief Change the node type in internal identifier.
 *
 * @param[in] intId    Internal identifier.
 * @param[in] nodeType New type to be set.
 * @return Internal identifier with new type.
 */
#define internalIdChangeType(intId, nodeType) \
	(((intId) & ~((quintptr) TYPE_MASK)) | (nodeType))

/*!
 * @brief Obtain node type from the internal identifier.
 *
 * @param[in] intId Internal identifier.
 * @return Node type.
 */
#define internalIdNodeType(intId) \
	((enum NodeType) ((intId) & TYPE_MASK))

/*!
 * @brief Obtain numeric account identifier from internal identifier.
 *
 * @param[in] indId Internal identifier.
 * @return Numeric equivalent of the account identifier.
 */
#define internalIdNumId(intId) \
	(((unsigned) (intId)) >> TYPE_BITS)

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

/* Null objects - for convenience. */
static const AcntId nullAcntId;

AccountModel::AccountModel(QObject *parent)
    : QAbstractItemModel(parent),
    m_accountsPtr(Q_NULLPTR),
    m_numIdMapping(),
    m_countersMap()
{
}

QModelIndex AccountModel::index(int row, int column,
    const QModelIndex &parent) const
{
	if (!hasIndex(row, column, parent)) {
		return QModelIndex();
	}

	enum NodeType type = nodeUnknown;

	/* Parent type. */
	if (!parent.isValid()) {
		type = nodeRoot;
	} else {
		type = nodeType(parent);
	}

	type = childNodeType(type, row); /* Child type. */

	if (nodeUnknown == type) {
		return QModelIndex();
	}

	quintptr internalId = 0;
	if (nodeAccountTop == type) {
		/* Set top node row and type. */
		Q_ASSERT(m_accountsPtr != Q_NULLPTR);
		Q_ASSERT((row >= 0) && (row < m_accountsPtr->acntIds().size()));
		Q_ASSERT(m_accountsPtr->acntIds().at(row).isValid());
		const int numId = m_numIdMapping.numId(m_accountsPtr->acntIds()[row]);
		Q_ASSERT(numId >= 0);
		Q_ASSERT(m_numIdMapping.acntId(numId).isValid());
		internalId = internalIdCreate(numId, type);
	} else {
		/* Preserve top node row from parent, change type. */
		internalId = internalIdChangeType(parent.internalId(), type);
	}

	return createIndex(row, column, internalId);
}

QModelIndex AccountModel::parent(const QModelIndex &index) const
{
	if (Q_UNLIKELY(!index.isValid())) {
		return QModelIndex();
	}

	quintptr internalId = index.internalId();

	/* Child type. */
	enum NodeType type = internalIdNodeType(internalId);

	int parentRow = -1;
	type = parentNodeType(type, &parentRow); /* Parent type. */

	if ((nodeUnknown == type) || (nodeRoot == type)) {
		return QModelIndex();
	}

	if (parentRow < 0) {
		Q_ASSERT(nodeAccountTop == type);
		Q_ASSERT(m_accountsPtr != Q_NULLPTR);
		/* Determine the row of the account top node. */
		const int numId = internalIdNumId(internalId);
		const AcntId acntId = m_numIdMapping.acntId(numId);
		int row = m_accountsPtr->acntIds().indexOf(acntId);
		if (row >= 0) {
			parentRow = row;
		}
	}

	Q_ASSERT(parentRow >= 0);

	/* Preserve top node row from child. */
	return createIndex(parentRow, 0,
	    internalIdChangeType(internalId, type));
}

int AccountModel::rowCount(const QModelIndex &parent) const
{
	if (Q_UNLIKELY(parent.column() > 0)) {
		return 0;
	}

	if (!parent.isValid()) {
		/* Root. */
		if (m_accountsPtr != Q_NULLPTR) {
			return m_accountsPtr->acntIds().size();
		} else {
			return 0;
		}
	}

	int rows = 0;

	switch (nodeType(parent)) {
	case nodeAccountTop:
		rows = 4;
		break;
	case nodeRecentReceived:
	case nodeRecentSent:
	case nodeDrafts:
		rows = 0;
		break;
	case nodeAll:
		rows = 2;
		break;
	case nodeReceived:
		{
			const AcntId &aId(acntId(parent));
			Q_ASSERT(aId.isValid());
			rows = m_countersMap[aId].receivedGroups.size();
		}
		break;
	case nodeSent:
		{
			const AcntId &aId(acntId(parent));
			Q_ASSERT(aId.isValid());
			rows = m_countersMap[aId].sentGroups.size();
		}
		break;
	default:
		rows = 0;
		break;
	}

	return rows;
}

int AccountModel::columnCount(const QModelIndex &parent) const
{
	Q_UNUSED(parent);

	return 1;
}

QVariant AccountModel::data(const QModelIndex &index, int role) const
{
	if (Q_UNLIKELY(!index.isValid())) {
		return QVariant();
	}

	const AcntId &aId(acntId(index));
	if (Q_UNLIKELY(!aId.isValid())) {
		Q_ASSERT(0);
		return QVariant();
	}
	const enum NodeType type = internalIdNodeType(index.internalId());

	const bool collapsed = role & ROLE_COLLAPSED_BIT;
	role = role & ~ROLE_COLLAPSED_BIT; /* Clear the collapse indicator bit. */

	switch (role) {
	case Qt::DisplayRole:
		switch (type) {
		case nodeAccountTop:
			if (!collapsed) {
				return m_accountsPtr->acntData(aId).accountName();
			} else {
				QString label = m_accountsPtr->acntData(aId).accountName();
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				if (cntrs.unreadRecentReceived > 0) {
					label += QString(" (%1)").arg(
					    cntrs.unreadRecentReceived);
				}
				return label;
			}
			break;
		case nodeRecentReceived:
			{
				QString label(tr("Recent Received"));
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				if (cntrs.unreadRecentReceived > 0) {
					label += QString(" (%1)").arg(
					    cntrs.unreadRecentReceived);
				}
				return label;
			}
			break;
		case nodeRecentSent:
			return tr("Recent Sent");
			break;
		case nodeDrafts:
			{
				QString label(tr("Drafts"));
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				if (cntrs.drafts > 0) {
					label += QString(" (%1)").arg(
					    cntrs.drafts);
				}
				return label;
			}
			break;
		case nodeAll:
			return tr("All");
			break;
		case nodeReceived:
			return tr("Received");
			break;
		case nodeSent:
			return tr("Sent");
			break;
		case nodeReceivedYear:
			{
				int row = index.row();
				Q_ASSERT(row >= 0);
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				Q_ASSERT(row < cntrs.receivedGroups.size());
				QString label(cntrs.receivedGroups[row]);
				Q_ASSERT(!label.isEmpty());
				Q_ASSERT(
				    cntrs.unreadReceivedGroups.constFind(label) !=
				    cntrs.unreadReceivedGroups.constEnd());
				if (cntrs.unreadReceivedGroups[label].unread > 0) {
					label += QString(" (%1)").arg(
					    cntrs.unreadReceivedGroups[label].unread);
				}
				return label;
			}
			break;
		case nodeSentYear:
			{
				int row = index.row();
				Q_ASSERT(row >= 0);
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				Q_ASSERT(row < cntrs.sentGroups.size());
				return cntrs.sentGroups[row];
			}
			break;
		default:
			Q_ASSERT(0);
			return QVariant();
			break;
		}
		break;

	case Qt::DecorationRole:
		{
			switch (type) {
			case nodeAccountTop:
				if (!collapsed) {
					return inconContainer.icon(IconContainer::ICON_DATABOX);
				} else {
					const AccountCounters &cntrs(
					    m_countersMap[aId]);
					return inconContainer.icon(
					    (cntrs.unreadRecentReceived > 0) ?
					        IconContainer::ICON_DATABOX_NEW_MESSAGE :
					        IconContainer::ICON_DATABOX);
				}
				break;
			case nodeRecentReceived:
			case nodeReceived:
			case nodeReceivedYear:
				return inconContainer.icon(IconContainer::ICON_MESSAGE_RECEIVED);
				break;
			case nodeRecentSent:
			case nodeSent:
			case nodeSentYear:
				return inconContainer.icon(IconContainer::ICON_MESSAGE_SENT);
				break;
			case nodeDrafts:
				return inconContainer.icon(IconContainer::ICON_APP_PENCIL);
				break;
			default:
				return QVariant();
				break;
			}
		}
		break;

	case Qt::FontRole:
		switch (type) {
		case nodeAccountTop:
			{
				QFont retFont;
				retFont.setBold(true);
				return retFont;
			}
			break;
		case nodeRecentReceived:
			{
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				if (cntrs.unreadRecentReceived > 0) {
					QFont retFont;
					retFont.setBold(true);
					return retFont;
				}
			}
			break;
		case nodeReceivedYear:
			{
				int row = index.row();
				Q_ASSERT(row >= 0);
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				Q_ASSERT(row < cntrs.receivedGroups.size());
				const QString &label(cntrs.receivedGroups[row]);
				Q_ASSERT(!label.isEmpty());
				Q_ASSERT(
				    cntrs.unreadReceivedGroups.constFind(label) !=
				    cntrs.unreadReceivedGroups.constEnd());
				if (cntrs.unreadReceivedGroups[label].unread > 0) {
					QFont retFont;
					retFont.setBold(true);
					return retFont;
				}
			}
			break;
		default:
			return QVariant();
			break;
		}
		break;

	case Qt::ForegroundRole:
		switch (type) {
		case nodeReceivedYear:
			{
				int row = index.row();
				const AccountCounters &cntrs(m_countersMap[aId]);
				const QString &label(cntrs.receivedGroups[row]);
				if (!cntrs.unreadReceivedGroups[label].dbOpened) {
					return QColor(Qt::darkGray);
				} else {
					return QVariant();
				}
			}
			break;
		case nodeSentYear:
			{
				int row = index.row();
				const AccountCounters &cntrs(m_countersMap[aId]);
				const QString &label(cntrs.sentGroups[row]);
				if (!cntrs.unreadSentGroups[label].dbOpened) {
					return QColor(Qt::darkGray);
				} else {
					return QVariant();
				}
			}
		default:
			return QVariant();
			break;
		}
		break;

	case Qt::AccessibleTextRole:
		switch (type) {
		case nodeAccountTop:
			return tr("data box") + QLatin1String(" ") +
			    m_accountsPtr->acntData(aId).accountName();
			break;
		case nodeRecentReceived:
			{
				QString label(tr("recently received messages"));
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				if (cntrs.unreadRecentReceived > 0) {
					label += QLatin1String(" - ") +
					    tr("contains %1 unread")
					        .arg(cntrs.unreadRecentReceived);
				}
				return label;
			}
			break;
		case nodeRecentSent:
			return tr("recently sent messages");
			break;
		case nodeDrafts:
			{
				QString label(tr("messages saved as drafts"));
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				if (cntrs.drafts > 0) {
					label += QLatin1String(" - ") +
					    tr("holds %n drafts(s)", "", cntrs.drafts);
				}
				return label;
			}
			break;
		case nodeAll:
			return tr("all messages");
			break;
		case nodeReceived:
			return tr("all received messages");
			break;
		case nodeSent:
			return tr("all sent messages");
			break;
		case nodeReceivedYear:
			{
				int row = index.row();
				Q_ASSERT(row >= 0);
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				Q_ASSERT(row < cntrs.receivedGroups.size());
				const QString &year(cntrs.receivedGroups[row]);
				if (year == MessageDb::invalidYearName) {
					return tr("invalid received messages");
				}
				QString label(
				    tr("messages received in year %1")
				        .arg(year));
				Q_ASSERT(!label.isEmpty());
				Q_ASSERT(
				    cntrs.unreadReceivedGroups.constFind(year) !=
				    cntrs.unreadReceivedGroups.constEnd());
				if (cntrs.unreadReceivedGroups[year].unread > 0) {
					label += QLatin1String(" - ") +
					    tr("contains %1 unread")
					        .arg(cntrs.unreadReceivedGroups[year].unread);
				}
				return label;
			}
			break;
		case nodeSentYear:
			{
				int row = index.row();
				Q_ASSERT(row >= 0);
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				Q_ASSERT(row < cntrs.sentGroups.size());
				if (cntrs.sentGroups[row] == MessageDb::invalidYearName) {
					return tr("invalid sent messages");
				}
				return tr("messages sent in year %1")
				    .arg(cntrs.sentGroups[row]);
			}
			break;
		default:
			return QVariant();
			break;
		}
		break;

	case ROLE_PLAIN_DISPLAY:
		switch (type) {
		case nodeReceivedYear:
			{
				int row = index.row();
				Q_ASSERT(row >= 0);
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				Q_ASSERT(row < cntrs.receivedGroups.size());
				return cntrs.receivedGroups[row];
			}
			break;
		case nodeSentYear:
			{
				int row = index.row();
				Q_ASSERT(row >= 0);
				Q_ASSERT(m_countersMap.constFind(aId) !=
				    m_countersMap.constEnd());
				const AccountCounters &cntrs(
				    m_countersMap[aId]);
				Q_ASSERT(row < cntrs.sentGroups.size());
				return cntrs.sentGroups[row];
			}
			break;
		default:
			return QVariant();
			break;
		}
		break;

	default:
		return QVariant();
		break;
	}

	return QVariant(); /* Is this really necessary? */
}

QVariant AccountModel::headerData(int section, Qt::Orientation orientation,
    int role) const
{
	if ((Qt::Horizontal == orientation) && (Qt::DisplayRole == role) &&
	    (0 == section)) {
		return tr("Data Boxes");
	}

	return QVariant();
}

bool AccountModel::moveRows(const QModelIndex &sourceParent, int sourceRow,
    int count, const QModelIndex &destinationParent, int destinationChild)
{
	if (sourceParent.isValid() || destinationParent.isValid()) {
		/* Only moves within root node are allowed. */
		return false;
	}

	return m_accountsPtr->moveAccounts(sourceRow, count,
	    destinationChild);
}

bool AccountModel::removeRows(int row, int count, const QModelIndex &parent)
{
	Q_UNUSED(row);
	Q_UNUSED(count);
	Q_UNUSED(parent);

	return false;
}

Qt::DropActions AccountModel::supportedDropActions(void) const
{
	/* The model must provide removeRows() to be able to use move action. */
	return Qt::MoveAction;
}

Qt::ItemFlags AccountModel::flags(const QModelIndex &index) const
{
	Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index);

	if (index.isValid()) {
		switch (nodeType(index)) {
		case nodeAccountTop:
			/* Allow drags on account top entries. */
			defaultFlags |= Qt::ItemIsDragEnabled;
			break;
		case nodeRecentReceived:
		case nodeRecentSent:
		case nodeDrafts:
		case nodeReceivedYear:
		case nodeSentYear:
			defaultFlags |= Qt::ItemNeverHasChildren;
			break;
		default:
			break;
		}
	} else {
		defaultFlags |= Qt::ItemIsDropEnabled;
	}

	return defaultFlags;
}

#define itemDataListMimeName \
	QLatin1String("application/x-qabstractitemmodeldatalist")

/* Custom MIME type. */
#define itemIndexRowListMimeName \
	QLatin1String("application/x-qitemindexrowlist")

QStringList AccountModel::mimeTypes(void) const
{
	return QStringList(itemIndexRowListMimeName);
}

QMimeData *AccountModel::mimeData(const QModelIndexList &indexes) const
{
	if (indexes.isEmpty()) {
		return Q_NULLPTR;
	}

	QMimeData *mimeData = new (::std::nothrow) QMimeData;
	if (Q_UNLIKELY(Q_NULLPTR == mimeData)) {
		return Q_NULLPTR;
	}

	/*
	 * TODO -- In order to encompass full account description then
	 * conversion to QVariant is needed.
	 * See QMimeData::setData() documentation.
	 */

	/* Convert row numbers into mime data. */
	QByteArray data;
	foreach (const QModelIndex &idx, indexes) {
		if (idx.isValid()) {
			qint64 row = idx.row();
			data.append((const char *)&row, sizeof(row));
		}
	}

	mimeData->setData(itemIndexRowListMimeName, data);

	return mimeData;
}

#if (QT_VERSION < QT_VERSION_CHECK(5, 4, 1))
/*
 * There are documented bugs with regard to
 * QAbstractItemModel::canDropMimeData() in Qt prior to version 5.4.1.
 * See QTBUG-32362 and QTBUG-30534.
 */
#warning "Compiling against version < Qt-5.4.1 which may have bugs around QAbstractItemModel::dropMimeData()."
#endif /* < Qt-5.4.1 */

bool AccountModel::canDropMimeData(const QMimeData *data, Qt::DropAction action,
    int row, int column, const QModelIndex &parent) const
{
	Q_UNUSED(row);
	Q_UNUSED(column);

	if ((Q_NULLPTR == data) || (Qt::MoveAction != action) ||
	    parent.isValid()) {
		return false;
	}

	return data->hasFormat(itemIndexRowListMimeName);
}

bool AccountModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
    int row, int column, const QModelIndex &parent)
{
	if (!canDropMimeData(data, action, row, column, parent)) {
		return false;
	}

	if (row < 0) {
		/*
		 * Dropping onto root node. This usually occurs when dropping
		 * on empty space behind last entry. Treat as dropping past
		 * last element.
		 */
		row = rowCount();
	}

	QList<int> droppedRows;
	{
		/* Convert mime data into row numbers. */
		QByteArray bytes(data->data(itemIndexRowListMimeName));
		qint64 row;
		const char *constBytes = bytes.constData();
		for (int offs = 0; offs < bytes.size(); offs += sizeof(row)) {
			row = *(qint64 *)(constBytes + offs);
			droppedRows.append(row);
		}
	}

	if (Q_UNLIKELY(droppedRows.isEmpty())) {
		logErrorNL("%s", "Got drop with no entries.");
		Q_ASSERT(0);
		return false;
	}

	if (droppedRows.size() > 1) {
		logWarningNL("%s",
		    "Cannot process drops of multiple entries at once.");
		return false;
	}

	moveRows(QModelIndex(), droppedRows.at(0), 1, parent, row);

	return false; /* If false is returned then removeRows() won't be triggered. */
}

void AccountModel::loadContent(AccountsMap *accounts)
{
	if (m_accountsPtr != Q_NULLPTR) {
		m_accountsPtr->disconnect(SIGNAL(accountAboutToBeAdded(AcntId, int)),
		    this, SLOT(watchAccountAboutToBeAdded(AcntId, int)));
		m_accountsPtr->disconnect(SIGNAL(accountAdded(AcntId, int)),
		    this, SLOT(watchAccountAdded(AcntId, int)));

		m_accountsPtr->disconnect(SIGNAL(accountAboutToBeRemoved(AcntId, int)),
		    this, SLOT(watchAccountAboutToBeRemoved(AcntId, int)));
		m_accountsPtr->disconnect(SIGNAL(accountRemoved(AcntId, int)),
		    this, SLOT(watchAccountRemoved(AcntId, int)));

		m_accountsPtr->disconnect(SIGNAL(accountsAboutToBeMoved(int, int, int)),
		    this, SLOT(watchAccountsAboutToBeMoved(int, int, int)));
		m_accountsPtr->disconnect(SIGNAL(accountsMoved(int, int, int)),
		    this, SLOT(watchAccountsMoved(int, int, int)));

		m_accountsPtr->disconnect(SIGNAL(accountUsernameChanged(AcntId, AcntId, int)),
		    this, SLOT(watchUsernameChanged(AcntId, AcntId, int)));
		m_accountsPtr->disconnect(SIGNAL(accountDataChanged(AcntId)),
		    this, SLOT(watchAcountChanged(AcntId)));
	}

	const QModelIndex parent;
	int rows = rowCount(parent);
	if (rows > 0) {
		beginRemoveRows(parent, 0, rows - 1);
		m_accountsPtr = Q_NULLPTR;
		m_numIdMapping.clear();
		m_countersMap.clear();
		endRemoveRows();
	}

	if (accounts == Q_NULLPTR) {
		return;
	}

	if (accounts->keys().isEmpty()) {
		m_accountsPtr = accounts;
	} else {
		rows = accounts->acntIds().size();
		beginInsertRows(parent, 0, rows - 1);
		m_accountsPtr = accounts;
		foreach (const AcntId &acntId, m_accountsPtr->acntIds()) {
			/* Prepare mapping and counters. */
			m_numIdMapping.addAcntId(acntId);
			m_countersMap[acntId] = AccountCounters();
		}
		endInsertRows();
	}

	connect(m_accountsPtr, SIGNAL(accountAboutToBeAdded(AcntId, int)),
	    this, SLOT(watchAccountAboutToBeAdded(AcntId, int)));
	connect(m_accountsPtr, SIGNAL(accountAdded(AcntId, int)),
	    this, SLOT(watchAccountAdded(AcntId, int)));

	connect(m_accountsPtr, SIGNAL(accountAboutToBeRemoved(AcntId, int)),
	    this, SLOT(watchAccountAboutToBeRemoved(AcntId, int)));
	connect(m_accountsPtr, SIGNAL(accountRemoved(AcntId, int)),
	    this, SLOT(watchAccountRemoved(AcntId, int)));

	connect(m_accountsPtr, SIGNAL(accountsAboutToBeMoved(int, int, int)),
	    this, SLOT(watchAccountsAboutToBeMoved(int, int, int)));
	connect(m_accountsPtr, SIGNAL(accountsMoved(int, int, int)),
	    this, SLOT(watchAccountsMoved(int, int, int)));

	connect(m_accountsPtr, SIGNAL(accountUsernameChanged(AcntId, AcntId, int)),
	    this, SLOT(watchUsernameChanged(AcntId, AcntId, int)));
	connect(m_accountsPtr, SIGNAL(accountDataChanged(AcntId)),
	    this, SLOT(watchAcountChanged(AcntId)));
}

const AcntId &AccountModel::acntId(const QModelIndex &index) const
{
	if (Q_UNLIKELY(!index.isValid())) {
		return nullAcntId;
	}

	const int numId = internalIdNumId(index.internalId());
	return m_numIdMapping.acntId(numId);
}

QModelIndex AccountModel::topAcntIndex(const AcntId &acntId) const
{
	int row = topAcntRow(acntId);
	if (row >= 0) {
		return index(row, 0);
	}

	return QModelIndex();
}

enum AccountModel::NodeType AccountModel::nodeType(const QModelIndex &index)
{
	if (!index.isValid()) {
		return nodeUnknown; /* nodeRoot ? */
	}

	/* TODO -- Add runtime value check? */
	return internalIdNodeType(index.internalId());
}

bool AccountModel::nodeTypeIsReceived(enum NodeType nodeType)
{
	switch (nodeType) {
	case nodeRecentReceived:
	case nodeReceived:
	case nodeReceivedYear:
		return true;
		break;
	default:
		return false;
		break;
	}
}

bool AccountModel::nodeTypeIsSent(enum NodeType nodeType)
{
	switch (nodeType) {
	case nodeRecentSent:
	case nodeSent:
	case nodeSentYear:
		return true;
		break;
	default:
		return false;
		break;
	}
}

bool AccountModel::nodeIsReceived(const QModelIndex &index)
{
	return nodeTypeIsReceived(nodeType(index));
}

bool AccountModel::nodeIsSent(const QModelIndex &index)
{
	return nodeTypeIsSent(nodeType(index));
}

bool AccountModel::updateRecentUnread(const AcntId &acntId,
    enum AccountModel::NodeType nodeType, unsigned unreadMsgs)
{
	if (Q_UNLIKELY(!acntId.isValid())) {
		Q_ASSERT(0);
		return false;
	}

	const QModelIndex topIndex(topAcntIndex(acntId));
	if (Q_UNLIKELY(!topIndex.isValid())) {
		return false;
	}

	AccountCounters &cntrs(m_countersMap[acntId]);
	unsigned *unreadRecent = 0;
	QModelIndex childIndex;

	if (nodeRecentReceived == nodeType) {
		unreadRecent = &cntrs.unreadRecentReceived;
		/* Get recently received node. */
		childIndex = index(CHROW_RECENT_RCVD, 0, topIndex);
	} else if (nodeRecentSent == nodeType) {
		unreadRecent = &cntrs.unreadRecentSent;
		/* Get recently sent node. */
		childIndex = index(CHROW_RECENT_SENT, 0, topIndex);
	} else {
		Q_ASSERT(0);
		return false;
	}

	Q_ASSERT(0 != unreadRecent);
	Q_ASSERT(childIndex.isValid());

	*unreadRecent = unreadMsgs;
	if (nodeRecentReceived == nodeType) {
		/*
		 * Recent received unread number can also be viewed in top node.
		 */
		Q_EMIT dataChanged(topIndex, topIndex);
	}
	Q_EMIT dataChanged(childIndex, childIndex);

	return true;
}

bool AccountModel::updateDraftCount(const AcntId &acntId, unsigned drafts)
{
	if (Q_UNLIKELY(!acntId.isValid())) {
		Q_ASSERT(0);
		return false;
	}

	const QModelIndex topIndex(topAcntIndex(acntId));
	if (Q_UNLIKELY(!topIndex.isValid())) {
		return false;
	}

	AccountCounters &cntrs(m_countersMap[acntId]);

	if (cntrs.drafts != drafts) {
		QModelIndex childIndex = index(CHROW_DRAFTS, 0, topIndex);

		cntrs.drafts = drafts;

		Q_EMIT dataChanged(childIndex, childIndex);
	}

	return true;
}

/*!
 * @brief Get position of the newly inserted year element.
 *
 * @param[in] yearList  List of years.
 * @param[in] addedYear Year To be added.
 * @param[in] sorting   Sorting.
 * @return Position of new element, -1 on any error.
 */
static
int addedYearPosistion(const QList<QString> &yearList, const QString &addedYear,
    enum AccountModel::Sorting sorting)
{
	static QRegularExpression yearRe("^[0-9][0-9][0-9][0-9]$");

	if (yearList.isEmpty()) {
		return 0;
	}

	bool addedIsYear = yearRe.match(addedYear).hasMatch();

	switch (sorting) {
	case AccountModel::UNSORTED:
		/* Just append. */
		return yearList.size();
		break;
	case AccountModel::ASCENDING:
		for (int pos = 0; pos < yearList.size(); ++pos) {
			const QString &yearEntry(yearList.at(pos));
			bool entryIsYear = yearRe.match(yearEntry).hasMatch();
			if (addedIsYear == entryIsYear) {
				if (addedYear < yearEntry) {
					return pos;
				}
			} else if (addedIsYear) {
				return pos; /* Years first. */
			}
		}
		return yearList.size();
		break;
	case AccountModel::DESCENDING:
		for (int pos = 0; pos < yearList.size(); ++pos) {
			const QString &yearEntry(yearList.at(pos));
			bool entryIsYear = yearRe.match(yearEntry).hasMatch();
			if (addedIsYear == entryIsYear) {
				if (addedYear > yearEntry) {
					return pos;
				}
			} else if (addedIsYear) {
				return pos; /* Years first. */
			}
		}
		return yearList.size();
		break;
	default:
		Q_ASSERT(0);
		return -1;
		break;
	}
}

bool AccountModel::updateYearNodes(const AcntId &acntId,
    enum AccountModel::NodeType nodeType,
    const QList< QPair<QString, YearCounter> > &yearlyUnreadList,
    enum AccountModel::Sorting sorting, bool prohibitYearRemoval)
{
	if (Q_UNLIKELY(!acntId.isValid())) {
		Q_ASSERT(0);
		return false;
	}

	AccountCounters &cntrs(m_countersMap[acntId]);
	QList<QString> *groups = Q_NULLPTR;
	QMap<QString, YearCounter> *unreadGroups = Q_NULLPTR;
	QModelIndex childTopIndex;

	{
		QModelIndex topIndex(topAcntIndex(acntId));
		if (!topIndex.isValid()) {
			return false;
		}

		if (nodeReceivedYear == nodeType) {
			groups = &cntrs.receivedGroups;
			unreadGroups = &cntrs.unreadReceivedGroups;
			/* Get received node. */
			childTopIndex = index(CHROW_ALL, 0, topIndex);
			childTopIndex = index(CHROW_RCVD, 0, childTopIndex);
		} else if (nodeSentYear == nodeType) {
			groups = &cntrs.sentGroups;
			unreadGroups = &cntrs.unreadSentGroups;
			/* Get sent node. */
			childTopIndex = index(CHROW_ALL, 0, topIndex);
			childTopIndex = index(CHROW_SENT, 0, childTopIndex);
		} else {
			Q_ASSERT(0);
			return false;
		}
	}
	Q_ASSERT(Q_NULLPTR != groups);
	Q_ASSERT(Q_NULLPTR != unreadGroups);
	Q_ASSERT(childTopIndex.isValid());

	/*
	 * Delete model elements that don't exist in new list or update
	 * existing entries.
	 */
	int row = groups->size() - 1;
	typedef QPair<QString, YearCounter> Pair;
	while (row >= 0) { /* In reverse order. */
		bool found = false;
		YearCounter yCounter;
		const QString &groupName(groups->at(row));
		foreach (const Pair &pair, yearlyUnreadList) {
			if (pair.first == groupName) {
				found = true;
				yCounter = pair.second;
				break;
			}
		}
		if (!prohibitYearRemoval && !found) {
			/* Remove row, don't increment row. */
			beginRemoveRows(childTopIndex, row, row);
			unreadGroups->remove(groupName);
			groups->removeAt(row);
			endRemoveRows();
		} else {
			/* Update unread, increment row. */
			(*unreadGroups)[groupName] = yCounter;
			QModelIndex childIndex(index(row, 0, childTopIndex));
			Q_EMIT dataChanged(childIndex, childIndex);
		}
		--row;
	}

	/* Add missing elements. */
	foreach (const Pair &pair, yearlyUnreadList) {
		if (!groups->contains(pair.first)) {
			int newRow = addedYearPosistion(*groups, pair.first,
			    sorting);
			if (Q_UNLIKELY(newRow < 0)) {
				Q_ASSERT(0);
				return false;
			}
			beginInsertRows(childTopIndex, newRow, newRow);
			groups->insert(newRow, pair.first);
			(*unreadGroups)[pair.first] = pair.second;
			endInsertRows();
		}
	}

	return true;
}

bool AccountModel::updateYear(const AcntId &acntId,
    enum AccountModel::NodeType nodeType, const QString &year,
    const YearCounter &yCounter)
{
	if (Q_UNLIKELY(!acntId.isValid())) {
		Q_ASSERT(0);
		return false;
	}

	QModelIndex topIndex(topAcntIndex(acntId));
	if (Q_UNLIKELY(!topIndex.isValid())) {
		return false;
	}

	AccountCounters &cntrs(m_countersMap[acntId]);
	QList<QString> *groups = Q_NULLPTR;
	QMap<QString, YearCounter> *unreadGroups = Q_NULLPTR;
	QModelIndex childIndex; /* Top index of children. */

	if (nodeReceivedYear == nodeType) {
		groups = &cntrs.receivedGroups;
		unreadGroups = &cntrs.unreadReceivedGroups;
		/* Get received node. */
		childIndex = index(CHROW_ALL, 0, topIndex); //
		childIndex = index(CHROW_RCVD, 0, childIndex);
	} else if (nodeSentYear == nodeType) {
		groups = &cntrs.sentGroups;
		unreadGroups = &cntrs.unreadSentGroups;
		/* Get sent node. */
		childIndex = index(CHROW_ALL, 0, topIndex);
		childIndex = index(CHROW_SENT, 0, childIndex);
	} else {
		Q_ASSERT(0);
		return false;
	}

	Q_ASSERT(Q_NULLPTR != groups);
	Q_ASSERT(Q_NULLPTR != unreadGroups);
	Q_ASSERT(childIndex.isValid());

	int row = 0;
	for (; row < groups->size(); ++row) {
		if (year == groups->at(row)) {
			break;
		}
	}
	if (row >= groups->size()) {
		return false;
	}
	childIndex = index(row, 0, childIndex);

	Q_ASSERT(childIndex.isValid());

	(*unreadGroups)[year] = yCounter;
	Q_EMIT dataChanged(childIndex, childIndex);

	return true;
}

void AccountModel::removeAllYearNodes(void)
{
	for (int row = 0; row < rowCount(); ++row) {
		QModelIndex topIndex(index(row, 0));

		Q_ASSERT(topIndex.isValid());

		removeYearNodes(topIndex);
	}
}

bool AccountModel::changePosition(const AcntId &acntId, int shunt)
{
	if (0 == shunt) {
		return false;
	}

	int row = topAcntRow(acntId);
	if (Q_UNLIKELY((row < 0) || (row >= m_accountsPtr->acntIds().size()))) {
		Q_ASSERT(0);
		return false;
	}
	int newRow = row + shunt;
	if ((newRow < 0) || (newRow >= m_accountsPtr->acntIds().size())) {
		return false;
	}

	int destChild = newRow;

	if (shunt > 0) {
		/*
		 * Because we move one item.
		 * See QAbstractItemModel::beginMoveRows() documentation.
		 */
		destChild += 1;
	}

	m_accountsPtr->moveAccounts(row, 1, destChild);

	return true;
}

void AccountModel::reloadIcons(void)
{
	inconContainer.rebuild();

	/* Collect all valid indexes. */
	QList<QModelIndex> indexes;
	{
		int i = 0;
		indexes.append(QModelIndex()); /* Top invalid node at index 0. */
		do {
			/* Don't use const reference here as the container may grow. */
			const QModelIndex parent = indexes.at(i);

			for (int row = 0; row < rowCount(parent); ++row) {
				for (int col = 0; col < columnCount(parent); ++col) {
					QModelIndex child = index(row, col, parent);
					if (child.isValid()) {
						indexes.append(child);
					} else {
						Q_ASSERT(0);
					}
				}
			}

			++i;
		} while (i < indexes.size());
	}

	/* Inform about decoration change. */
	for (int i = 1; i < indexes.size(); ++i) { /* Top invalid node at index 0. */
		const QModelIndex &index = indexes.at(i);
		Q_EMIT dataChanged(index, index, {Qt::DecorationRole});
	}
}

void AccountModel::watchAccountAboutToBeAdded(const AcntId &acntId, int row)
{
	beginInsertRows(QModelIndex(), row, row);

	m_numIdMapping.addAcntId(acntId);
	m_countersMap[acntId] = AccountCounters();
}

void AccountModel::watchAccountAdded(const AcntId &acntId, int row)
{
	Q_UNUSED(acntId);
	Q_UNUSED(row);

	endInsertRows();
}

void AccountModel::watchAccountAboutToBeRemoved(const AcntId &acntId, int row)
{
	beginRemoveRows(QModelIndex(), row, row);

	m_numIdMapping.removeAcntId(acntId);
	m_countersMap.remove(acntId);
}

void AccountModel::watchAccountRemoved(const AcntId &acntId, int row)
{
	Q_UNUSED(acntId);
	Q_UNUSED(row);

	endRemoveRows();
}

void AccountModel::watchAccountsAboutToBeMoved(int srcFrst, int srcLst, int dst)
{
	const QModelIndex parent;

	beginMoveRows(parent, srcFrst, srcLst, parent, dst);
}

void AccountModel::watchAccountsMoved(int srcFrst, int srcLst, int dst)
{
	Q_UNUSED(srcFrst);
	Q_UNUSED(srcLst);
	Q_UNUSED(dst);

	endMoveRows();
}

void AccountModel::watchUsernameChanged(const AcntId &oldId,
    const AcntId &newId, int index)
{
	Q_UNUSED(index);

	m_numIdMapping.replaceAcntId(oldId, newId);
	m_countersMap.insert(newId, m_countersMap.take(oldId));

	const QModelIndex topIndex(topAcntIndex(newId));
	if (topIndex.isValid()) {
		Q_EMIT dataChanged(topIndex, topIndex);
	}
}

void AccountModel::watchAcountChanged(const AcntId &acntId)
{
	const QModelIndex topIndex(topAcntIndex(acntId));

	if (topIndex.isValid()) {
		Q_EMIT dataChanged(topIndex, topIndex);
	}
}

void AccountModel::removeYearNodes(const QModelIndex &topIndex)
{
	Q_ASSERT(topIndex.isValid());

	const AcntId aId(acntId(topIndex));
	Q_ASSERT(aId.isValid());

	AccountCounters &cntrs(m_countersMap[aId]);

	/* Received. */
	QModelIndex childTopIndex = index(CHROW_ALL, 0, topIndex);
	childTopIndex = index(CHROW_RCVD, 0, childTopIndex);
	int rows = rowCount(childTopIndex);
	if (rows > 0) {
		beginRemoveRows(childTopIndex, 0, rows - 1);
		cntrs.receivedGroups.clear();
		cntrs.unreadReceivedGroups.clear();
		endRemoveRows();
	}

	/* Sent. */
	childTopIndex = index(CHROW_ALL, 0, topIndex);
	childTopIndex = index(CHROW_SENT, 0, childTopIndex);
	rows = rowCount(childTopIndex);
	if (rows > 0) {
		beginRemoveRows(childTopIndex, 0, rows - 1);
		cntrs.sentGroups.clear();
		cntrs.unreadSentGroups.clear();
		endRemoveRows();
	}
}

enum AccountModel::NodeType AccountModel::childNodeType(
    enum AccountModel::NodeType parentType, int childRow)
{
	switch (parentType) {
	case nodeUnknown:
		return nodeUnknown;
		break;
	case nodeRoot:
		return nodeAccountTop;
		break;
	case nodeAccountTop:
		switch (childRow) {
		case CHROW_RECENT_RCVD:
			return nodeRecentReceived;
			break;
		case CHROW_RECENT_SENT:
			return nodeRecentSent;
			break;
		case CHROW_DRAFTS:
			return nodeDrafts;
			break;
		case CHROW_ALL:
			return nodeAll;
			break;
		default:
			return nodeUnknown;
			break;
		}
		break;
	case nodeRecentReceived:
	case nodeRecentSent:
	case nodeDrafts:
		return nodeUnknown;
		break;
	case nodeAll:
		switch (childRow) {
		case CHROW_RCVD:
			return nodeReceived;
			break;
		case CHROW_SENT:
			return nodeSent;
			break;
		default:
			return nodeUnknown;
			break;
		}
		break;
	case nodeReceived:
		return nodeReceivedYear;
		break;
	case nodeSent:
		return nodeSentYear;
		break;
	case nodeReceivedYear:
	case nodeSentYear:
		return nodeUnknown;
		break;
	default:
		Q_ASSERT(0);
		return nodeUnknown;
		break;
	}
}

enum AccountModel::NodeType AccountModel::parentNodeType(
    enum AccountModel::NodeType childType, int *parentRow)
{
	switch (childType) {
	case nodeUnknown:
	case nodeRoot:
		if (Q_NULLPTR != parentRow) {
			*parentRow = CHROW_NONE;
		}
		return nodeUnknown;
		break;
	case nodeAccountTop:
		if (Q_NULLPTR != parentRow) {
			*parentRow = CHROW_ROOT;
		}
		return nodeRoot;
		break;
	case nodeRecentReceived:
	case nodeRecentSent:
	case nodeDrafts:
	case nodeAll:
		if (Q_NULLPTR != parentRow) {
			*parentRow = CHROW_ACNT_TOP;
		}
		return nodeAccountTop;
		break;
	case nodeReceived:
	case nodeSent:
		if (Q_NULLPTR != parentRow) {
			*parentRow = CHROW_ALL;
		}
		return nodeAll;
		break;
	case nodeReceivedYear:
		if (Q_NULLPTR != parentRow) {
			*parentRow = CHROW_RCVD;
		}
		return nodeReceived;
		break;
	case nodeSentYear:
		if (Q_NULLPTR != parentRow) {
			*parentRow = CHROW_SENT;
		}
		return nodeSent;
		break;
	default:
		Q_ASSERT(0);
		if (Q_NULLPTR != parentRow) {
			*parentRow = CHROW_NONE;
		}
		return nodeUnknown;
		break;
	}
}

int AccountModel::topAcntRow(const AcntId &acntId) const
{
	int foundRow = -1;

	if (Q_UNLIKELY(m_accountsPtr == Q_NULLPTR)) {
		Q_ASSERT(0);
		return foundRow;
	}

	for (int row = 0; row < m_accountsPtr->acntIds().size(); ++row) {
		if (acntId == m_accountsPtr->acntIds().at(row)) {
			foundRow = row;
			break;
		}
	}

	return foundRow;
}
