/*
 * 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 "src/datovka_shared/compat/compiler.h" /* macroStdMove */
#include "src/io/tag_container.h"
#include "src/json/message_id.h"
#include "src/json/tag_assignment_hash.h"
#include "src/models/tag_assignment_model.h"

#define WRONG_TAG_ID -1 /** TODO -- Remove. */

TagAssignmentModel::TagAssignmentModel(TagContainer *tagCont,
    QObject *parent)
    : QAbstractListModel(parent),
    m_tagContPtr(tagCont),
    m_acntId(),
    m_msgIds(),
    m_tagList(),
    m_assignedTags(),
    m_msgTagItemsMap()
{
	if (Q_NULLPTR != m_tagContPtr) {
		loadContent();

		connect(m_tagContPtr, SIGNAL(tagsInserted(Json::TagEntryList)),
		    this, SLOT(watchTagsInserted(Json::TagEntryList)));
		connect(m_tagContPtr, SIGNAL(tagsUpdated(Json::TagEntryList)),
		    this, SLOT(watchTagsUpdated(Json::TagEntryList)));
		connect(m_tagContPtr, SIGNAL(tagsDeleted(Json::Int64StringList)),
		    this, SLOT(watchTagsDeleted(Json::Int64StringList)));
		connect(m_tagContPtr, SIGNAL(tagAssignmentChanged(Json::TagAssignmentList)),
		    this, SLOT(watchTagAssignmentChanged(Json::TagAssignmentList)));
		connect(m_tagContPtr, SIGNAL(connected()),
		    this, SLOT(watchTagContConnected()));
		connect(m_tagContPtr, SIGNAL(disconnected()),
		    this, SLOT(watchTagContDisconnected()));
		connect(m_tagContPtr, SIGNAL(reset()),
		    this, SLOT(watchTagContReset()));
	}
}

int TagAssignmentModel::rowCount(const QModelIndex &parent) const
{
	if (parent.isValid()) {
		return 0;
	} else {
		return m_tagList.size();
	}
}

/*!
 * @brief Converts list of tag items to a set.
 *
 * @param[in] list Tag item list.
 * @return Tag item set.
 */
static inline
QSet<TagItem> itemListToSet(const QList<TagItem> &list)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
	return QSet<TagItem>(list.constBegin(), list.constEnd());
#else /* < Qt-5.14.0 */
	return list.toSet();
#endif /* >= Qt-5.14.0 */
}

/*!
 * @brief Converts set of tag items to a list.
 *
 * @param[in] set Tag item set.
 * @return Tag item list.
 */
static inline
QList<TagItem> itemSetToList(const QSet<TagItem> &set)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
	return QList<TagItem>(set.constBegin(), set.constEnd());
#else /* < Qt-5.14.0 */
	return set.toList();
#endif /* >= Qt-5.14.0 */
}

/*!
 * @brief Computes the union and intersection of the tags in the lists over all
 *     supplied message identifiers.
 *
 * @param[in] msgTagItemsMap Mapping of message IDs to assigned tag lists.
 * @param[in] tagsUnion Union of all tags.
 * @param[in] tagsIntersection Intersection of all tags.
 */
static
void computeTagSets(const QHash<Json::MsgId1, TagItemList> &msgTagItemsMap,
    QSet<TagItem> &tagsUnion, QSet<TagItem> &tagsIntersection)
{
	tagsUnion.clear();
	tagsIntersection.clear();

	bool firstMsg = true;
	QHash<Json::MsgId1, TagItemList>::const_iterator it;
	for (it = msgTagItemsMap.constBegin(); it != msgTagItemsMap.constEnd(); ++it) {
		const QSet<TagItem> msgTagSet = itemListToSet(*it);

		tagsUnion += msgTagSet;
		if (firstMsg) {
			tagsIntersection = msgTagSet;
			firstMsg = false;
		} else {
			tagsIntersection.intersect(msgTagSet);
		}
	}
}

/*!
 * @brief Get tag id from index.
 *
 * @param[in] idx Model index.
 * @return Tag id if success else -1.
 */
static inline
qint64 getTagIdFromIndex(const QModelIndex &idx)
{
	if (Q_UNLIKELY(!idx.isValid())) {
		return WRONG_TAG_ID;
	}

	if (Q_UNLIKELY(!idx.data().canConvert<TagItem>())) {
		return WRONG_TAG_ID;
	}

	TagItem tagItem(qvariant_cast<TagItem>(idx.data()));

	return tagItem.id();
}

QVariant TagAssignmentModel::data(const QModelIndex &index, int role) const
{
	if (Q_UNLIKELY((index.row() >= m_tagList.size())
	        && (index.column() != COL_TAG_NAME))) {
		return QVariant();
	}

	switch (role) {
	case Qt::DisplayRole:
		return QVariant::fromValue(m_tagList.at(index.row()));
		break;
	case Qt::CheckStateRole:
		{
			QSet<TagItem> intersection = itemListToSet(m_tagList);
			intersection.intersect(m_assignedTags);

			return intersection.contains(m_tagList.at(index.row()))
			    ? Qt::Checked : Qt::Unchecked;
		}
		break;
	case ROLE_PROXYSORT:
		return m_tagList.at(index.row()).name();
		break;
	default:
		break;
	}

	return QVariant();
}

Qt::ItemFlags TagAssignmentModel::flags(const QModelIndex &index) const
{
	Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);

	/*
	 * Enable tag assignment only when index is valid and some message ids
	 * have been provided.
	 */
	if (index.isValid() && (!m_msgIds.isEmpty())) {
		defaultFlags |= Qt::ItemIsUserCheckable;
	}

	return defaultFlags;
}

bool TagAssignmentModel::setData(const QModelIndex &index,
    const QVariant &value, int role)
{
	if (Q_UNLIKELY(!index.isValid())) {
		return false;
	}

	const int row = index.row();
	if (Q_UNLIKELY((row < 0) || (row >= rowCount()))) {
		Q_ASSERT(0);
		return false;
	}

	if ((index.column() != COL_TAG_NAME) || (role != Qt::CheckStateRole)) {
		return QAbstractListModel::setData(index, value, role);
	}

	Q_ASSERT(m_acntId.isValid());

	const bool assign = (value == Qt::Checked);
	if (assign) {
		/* Assign tags. */
		Json::Int64StringList tagIds;
		tagIds.append(m_tagList.at(row).id());
		{
			Json::TagAssignmentCommand tagAssignmentCommand;
			for (const Json::MsgId1 &msgId : m_msgTagItemsMap.keys()) {
				tagAssignmentCommand[msgId] = tagIds;
			}
			m_tagContPtr->assignTagsToMsgs(tagAssignmentCommand);
		}
		return true;
	} else {
		/* Remove assignment. */
		Json::Int64StringList tagIds;
		tagIds.append(m_tagList.at(row).id());
		{
			Json::TagAssignmentCommand tagAssignmentCommand;
			for (const Json::MsgId1 &msgId : m_msgTagItemsMap.keys()) {
				tagAssignmentCommand[msgId] = tagIds;
			}
			m_tagContPtr->removeTagsFromMsgs(tagAssignmentCommand);
		}
		return true;
	}

	return false;
}

QVariant TagAssignmentModel::headerData(int section,
    Qt::Orientation orientation, int role) const
{
	Q_UNUSED(orientation);

	if (role != Qt::DisplayRole) {
		return QVariant();
	}

	if (COL_TAG_NAME == section) {
		return tr("Tags");
	} else {
		return QVariant();
	}
}

void TagAssignmentModel::setMsgIds(const AcntId &acntId,
    const QSet<qint64> &msgIds)
{
	beginResetModel();
	m_acntId = acntId;
	m_msgIds = msgIds;

	loadAssignments();

	endResetModel();
}

void TagAssignmentModel::setTagList(const TagItemList &tagList)
{
	beginResetModel();

	m_tagList = tagList;
	/* Keep tags ordered. */
	m_tagList.sortNames();

	endResetModel();
}

void TagAssignmentModel::insertTag(const Json::TagEntry &entry)
{
	if (Q_UNLIKELY(!entry.isValid())) {
		Q_ASSERT(0);
		return;
	}

	if (Q_UNLIKELY(m_tagList.containsId(entry.id()))) {
		return;
	}

	/* Find proper place in ordered list. */
	TagItem item(entry);
	TagItemList::iterator it = m_tagList.upperBoundName(item);
	int pos = it - m_tagList.begin(); /* Get position to be inserted. */

	beginInsertRows(QModelIndex(), pos, pos);

	m_tagList.insert(it, item);

	endInsertRows();
}

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

	int oldPos = m_tagList.indexOfId(entry.id());
	if (Q_UNLIKELY(oldPos < 0)) {
		return;
	}

	int newPos = 0;
	{
		TagItem item(entry);
		TagItemList::iterator it = m_tagList.upperBoundName(item);
		newPos = it - m_tagList.begin(); /* Get position to be inserted. */
	}

	if ((oldPos + 1) == newPos) {
		/*
		 * New position is just past the old position.
		 * Technically the position does not change in this case.
		 */
		newPos = oldPos;
	} else if (oldPos != newPos) {
		/* Moved to different position. */

		/*
		 * QAbstractItemModel::beginMoveRows() behaves slightly
		 * differently to QList::move().
		 */
		beginMoveRows(QModelIndex(), oldPos, oldPos, QModelIndex(), newPos);

		if (oldPos < newPos) {
			/*
			 * New position was computed behind the element which
			 * is moved and therefore technically not being
			 * present in it's position.
			 */
			--newPos;
		}

		m_tagList.move(oldPos, newPos);

		endMoveRows();
	}

	m_tagList[newPos] = TagItem(entry);

	Q_EMIT dataChanged(index(newPos, COL_TAG_NAME), index(newPos, COL_TAG_NAME));
}

void TagAssignmentModel::deleteTag(qint64 id)
{
	if (Q_UNLIKELY(id < 0)) {
		Q_ASSERT(0);
		return;
	}

	int pos = m_tagList.indexOfId(id);
	if (Q_UNLIKELY(pos < 0)) {
		return;
	}

	beginRemoveRows(QModelIndex(), pos, pos);

	m_tagList.removeAt(pos);

	endRemoveRows();
}

void TagAssignmentModel::watchTagsInserted(const Json::TagEntryList &entries)
{
	for (const Json::TagEntry &entry : entries) {
		if (Q_UNLIKELY(!entry.isValid())) {
			continue;
		}

		insertTag(entry);
	}
}

void TagAssignmentModel::watchTagsUpdated(const Json::TagEntryList &entries)
{
	for (const Json::TagEntry &entry : entries) {
		if (Q_UNLIKELY(!entry.isValid())) {
			continue;
		}

		updateTag(entry);
	}
}

void TagAssignmentModel::watchTagsDeleted(const Json::Int64StringList &ids)
{
	for (qint64 id : ids) {
		if (Q_UNLIKELY(id < 0)) {
			continue;
		}

		deleteTag(id);
	}
}

void TagAssignmentModel::watchTagAssignmentChanged(
    const Json::TagAssignmentList &assignments)
{
	bool updated = false;
	for (const Json::TagAssignment &assignment : assignments) {
		/* Do nothing if account has been changed. */
		if (Q_UNLIKELY(assignment.testEnv() != m_acntId.testing())) {
			continue;
		}

		QHash<Json::MsgId1, TagItemList>::iterator it =
		    m_msgTagItemsMap.find(Json::MsgId1(
		        assignment.testEnv(), assignment.dmId()));
		if (it == m_msgTagItemsMap.end()) {
			continue;
		}

		*it = assignment.entries();
		updated = true;
	}

	if (updated) {
		/*
		 * Get set of tags assigned to any supplied message and to all supplied
		 * messages.
		 */
		QSet<TagItem> assignedTagsUnion, assignedTagsIntersection;
		computeTagSets(m_msgTagItemsMap, assignedTagsUnion, assignedTagsIntersection);
		{
			m_assignedTags = macroStdMove(assignedTagsUnion);
		}
		Q_EMIT dataChanged(index(0, COL_TAG_NAME),
		    index(rowCount() - 1, COL_TAG_NAME), {Qt::CheckStateRole});
	}
}

void TagAssignmentModel::watchTagContConnected(void)
{
	loadContent();
}

void TagAssignmentModel::watchTagContDisconnected(void)
{
	setTagList(Json::TagEntryList());
}

void TagAssignmentModel::watchTagContReset(void)
{
	setTagList(Json::TagEntryList());
	loadContent();
}

void TagAssignmentModel::loadAssignments(void)
{
	if (Q_UNLIKELY((!m_acntId.isValid()) || (m_msgIds.isEmpty()))) {
		/*
		 * Can be called without account and empty message list.
		 * Clearing assignments if done so.
		 */
		m_assignedTags.clear();
		m_msgTagItemsMap.clear();
		return;
	}

	Json::MsgId1List jsonMsgIdList;
	for (qint64 msgId : m_msgIds) {
		jsonMsgIdList.append(Json::MsgId1(
		    m_acntId.testing() ? Isds::Type::BOOL_TRUE : Isds::Type::BOOL_FALSE,
		    msgId));
	}

	/* Fill map of tag item lists. */
	m_msgTagItemsMap.clear();
	{
		Json::TagAssignmentHash assignments;
		if (m_tagContPtr->getMessageTags(jsonMsgIdList, assignments)) {
			for (const Json::MsgId1 &msgId : assignments.keys()) {
				m_msgTagItemsMap[msgId] = assignments[msgId];
			}
		}
	}
	/*
	 * Get set of tags assigned to any supplied message and to all supplied
	 * messages.
	 */
	QSet<TagItem> assignedTagsUnion, assignedTagsIntersection;
	computeTagSets(m_msgTagItemsMap, assignedTagsUnion, assignedTagsIntersection);
	{
		m_assignedTags = macroStdMove(assignedTagsUnion);
	}
}

void TagAssignmentModel::loadContent(void)
{
	/* Get all available tags. */
	Json::TagEntryList availableTags;
	m_tagContPtr->getAllTags(availableTags);
	setTagList(availableTags);

	loadAssignments();
}
