/*
 * 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 <QItemSelectionModel>
#include <QMessageBox>
#include <QString>

#include "src/delegates/tag_item.h"
#include "src/dimensions/dimensions.h"
#include "src/global.h"
#include "src/gui/dlg_tag.h"
#include "src/gui/dlg_tags.h"
#include "src/gui/icon_container.h"
#include "src/gui/styles.h"
#include "src/io/tag_container.h"
#include "src/json/message_id.h"
#include "src/json/tag_assignment_hash.h"
#include "src/settings/prefs_specific.h"
#include "src/views/table_key_press_filter.h"
#include "ui_dlg_tags.h"

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

static const QString dlgName("tags");

DlgTags::DlgTags(const AcntId &acntId, TagContainer *tagCont,
    const QList<qint64> &msgIdList, QWidget *parent)
    : QDialog(parent),
    m_ui(new (::std::nothrow) Ui::DlgTags),
    m_acntId(acntId),
    m_msgIdList(msgIdList),
    m_tagContPtr(tagCont),
    m_msgTagItemsMap(),
    m_availableTagsDelegate(this),
    m_availableTagsModel(this),
    m_assignedTagsDelegate(this),
    m_assignedTagsModel(this),
    m_retCode(NO_ACTION)
{
	m_ui->setupUi(this);
	/* Tab order is defined in UI file. */

	initDlg(!m_msgIdList.isEmpty());
	loadModels();
	//selectAllAssingedTagsFromMsgs();
}

DlgTags::~DlgTags(void)
{
	delete m_ui;
}

enum DlgTags::ReturnCode DlgTags::editAvailable(TagContainer *tagCont,
    QWidget *parent)
{
	if (Q_UNLIKELY(tagCont == Q_NULLPTR)) {
		Q_ASSERT(0);
		return NO_ACTION;
	}

	DlgTags dlg(AcntId(), tagCont, QList<qint64>(), parent);

	const QSize dfltSize = dlg.size();
	{
		const QSize newSize = Dimensions::dialogueSize(&dlg,
		    PrefsSpecific::dlgSize(*GlobInstcs::prefsPtr, dlgName),
		    dfltSize);
		if (newSize.isValid()) {
			dlg.resize(newSize);
		}
	}

	dlg.exec();

	PrefsSpecific::setDlgSize(*GlobInstcs::prefsPtr, dlgName,
	    dlg.size(), dfltSize);

	return dlg.m_retCode;
}

enum DlgTags::ReturnCode DlgTags::editAssignment(const AcntId &acntId,
    TagContainer *tagCont, const QList<qint64> &msgIdList, QWidget *parent)
{
	if (Q_UNLIKELY((tagCont == Q_NULLPTR) || msgIdList.isEmpty())) {
		Q_ASSERT(0);
		return NO_ACTION;
	}

	DlgTags dlg(acntId, tagCont, msgIdList, parent);

	const QSize dfltSize = dlg.size();
	{
		const QSize newSize = Dimensions::dialogueSize(&dlg,
		    PrefsSpecific::dlgSize(*GlobInstcs::prefsPtr, dlgName),
		    dfltSize);
		if (newSize.isValid()) {
			dlg.resize(newSize);
		}
	}

	dlg.exec();

	PrefsSpecific::setDlgSize(*GlobInstcs::prefsPtr, dlgName,
	    dlg.size(), dfltSize);

	return dlg.m_retCode;
}

void DlgTags::addTag(void)
{
	DlgTag::createTag(m_tagContPtr, this);

	/* Tag list is updated according to received signals. */
}

/*!
 * @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();
}

/*!
 * @brief Get selected index.
 *
 * @param[in] view List view.
 * @return Selected index.
 */
static inline
QModelIndex selectedIndex(const QListView *view)
{
	if (Q_UNLIKELY(view == Q_NULLPTR)) {
		Q_ASSERT(0);
		return QModelIndex();
	}

	return view->selectionModel()->currentIndex();
}

void DlgTags::updateTag(void)
{
	Json::TagEntry entry;
	{
		Json::Int64StringList ids(
		    {getTagIdFromIndex(selectedIndex(m_ui->availableTagsView))});
		Json::TagEntryList entries;
		if (Q_UNLIKELY(!m_tagContPtr->getTagData(ids, entries))) {
			return;
		}
		if (Q_UNLIKELY(entries.size() != 1)) {
			return;
		}
		entry = entries.at(0);
	}
	TagItem tagItem(entry);

	if (DlgTag::editTag(m_tagContPtr, tagItem, this)) {
		/* Existing tag has very likely just been changed. */
		m_retCode = TAGS_CHANGED;
	}
}

/*!
 * @brief Get selected indexes.
 *
 * @param[in] view List view.
 * @return Selected indexes.
 */
static inline
QModelIndexList selectedIndexes(const QListView *view)
{
	if (Q_UNLIKELY(view == Q_NULLPTR)) {
		Q_ASSERT(0);
		return QModelIndexList();
	}

	return view->selectionModel()->selectedRows();
}

void DlgTags::deleteTag(void)
{
	QModelIndexList slctIdxs(selectedIndexes(m_ui->availableTagsView));
	if (Q_UNLIKELY(slctIdxs.isEmpty())) {
		/* Nothing to do. */
		return;
	}

	Json::Int64StringList usedTagIds;
	{
		Json::Int64StringList tagIds;
		Json::TagIdToAssignmentCountHash assignedCounts;
		for (const QModelIndex &idx : slctIdxs) {
			tagIds.append(getTagIdFromIndex(idx));
		}
		m_tagContPtr->getTagAssignmentCounts(tagIds, assignedCounts);
		/* Collects tag identifiers with non-zero assignment counts. */
		for (qint64 tagId : assignedCounts.keys()) {
			if (assignedCounts.value(tagId, 0) > 0) {
				usedTagIds.append(tagId);
			}
		}
	}

	if ((usedTagIds.size() > 0)) {
		QMessageBox::StandardButton reply = QMessageBox::question(this,
		    tr("Delete assigned tags"),
		    tr("Some selected tags are assigned to some messages.") +
		    "<br/><br/>" +
		    tr("Do you want to delete the selected tags?"),
		    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
		if (reply == QMessageBox::No) {
			return;
		}
	}

	Json::Int64StringList ids;
	for (const QModelIndex &idx : slctIdxs) {
		ids.append(getTagIdFromIndex(idx));
	}

	m_tagContPtr->deleteTags(ids);

	/* Existing tags have been removed. */
	m_retCode = TAGS_CHANGED;
}

void DlgTags::assignSelectedTagsToMsgs(void)
{
	QModelIndexList slctIdxs(selectedIndexes(m_ui->availableTagsView));
	if (Q_UNLIKELY(slctIdxs.isEmpty())) {
		/* Nothing to do. */
		return;
	}

	Q_ASSERT(m_acntId.isValid());

	Json::Int64StringList tagIds;
	for (const QModelIndex &idx : slctIdxs) {
		tagIds.append(getTagIdFromIndex(idx));
	}

	{
		Json::TagAssignmentCommand tagAssignmentCommand;
		for (const Json::MsgId1 &msgId : m_msgTagItemsMap.keys()) {
			tagAssignmentCommand[msgId] = tagIds;
		}
		m_tagContPtr->assignTagsToMsgs(tagAssignmentCommand);
	}

	/* Tag assignment was changed. */
	if (m_retCode != TAGS_CHANGED) {
		m_retCode = ASSIGMENT_CHANGED;
	}
}

void DlgTags::removeSelectedTagsFromMsgs(void)
{
	QModelIndexList slctIdxs(selectedIndexes(m_ui->assignedTagsView));
	if (Q_UNLIKELY(slctIdxs.isEmpty())) {
		/* Nothing to do. */
		return;
	}

	Q_ASSERT(m_acntId.isValid());

	/* Clear the selection before removing content. */
	m_ui->assignedTagsView->clearSelection();

	Json::Int64StringList tagIds;
	for (const QModelIndex &idx : slctIdxs) {
		tagIds.append(getTagIdFromIndex(idx));
	}

	{
		Json::TagAssignmentCommand tagAssignmentCommand;
		for (const Json::MsgId1 &msgId : m_msgTagItemsMap.keys()) {
			tagAssignmentCommand[msgId] = tagIds;
		}
		m_tagContPtr->removeTagsFromMsgs(tagAssignmentCommand);
	}

	/* Tag assignment was changed. */
	if (m_retCode != TAGS_CHANGED) {
		m_retCode = ASSIGMENT_CHANGED;
	}
}

void DlgTags::removeAllTagsFromMsgs(void)
{
	Q_ASSERT(m_acntId.isValid());

	/* Clear the selection before removing content. */
	m_ui->assignedTagsView->clearSelection();

	m_tagContPtr->removeAllTagsFromMsgs(m_msgTagItemsMap.keys());

	/* Tag assignment was changed. */
	if (m_retCode != TAGS_CHANGED) {
		m_retCode = ASSIGMENT_CHANGED;
	}
}

void DlgTags::handleAvailableSelectionChange(void)
{
	QModelIndexList slctIdxs(selectedIndexes(m_ui->availableTagsView));

	m_ui->deleteTagButton->setEnabled(!slctIdxs.isEmpty());
	m_ui->updateTagButton->setEnabled(slctIdxs.count() == 1);
	m_ui->assignButton->setEnabled(!slctIdxs.isEmpty());

	if (!slctIdxs.isEmpty()) {
		m_ui->assignedTagsView->clearSelection();
	}
}

void DlgTags::handleAssignedSelectionChange(void)
{
	QModelIndexList slctIdxs(selectedIndexes(m_ui->assignedTagsView));

	m_ui->removeButton->setEnabled(!slctIdxs.isEmpty());

	if (!slctIdxs.isEmpty()) {
		m_ui->availableTagsView->clearSelection();
	}
}

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

		m_availableTagsModel.insertTag(entry);
	}
}

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

		m_availableTagsModel.updateTag(entry);
		m_assignedTagsModel.updateTag(entry);
	}
}

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

		m_availableTagsModel.deleteTag(id);
		m_assignedTagsModel.deleteTag(id);
	}
}

/*!
 * @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);
		}
	}
}

void DlgTags::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);
		{
			QList<TagItem> assignedTagList = itemSetToList(assignedTagsUnion);
			m_assignedTagsModel.setTagList(assignedTagList);
			m_ui->removeAllButton->setEnabled(assignedTagList.count() > 0);
		}
	}
}

void DlgTags::watchTagContConnected(void)
{
	loadModels();
}

void DlgTags::watchTagContDisconnected(void)
{
	clearModels();
}

void DlgTags::watchTagContReset(void)
{
	clearModels();
	loadModels();
}

void DlgTags::loadAssignments(void)
{
	if (Q_UNLIKELY((!m_acntId.isValid()) || (m_msgIdList.isEmpty()))) {
		/*
		 * Can be called without account and empty message list.
		 * Clearing assignments if done so.
		 */
		m_assignedTagsModel.setTagList(QList<TagItem>());
		m_msgTagItemsMap.clear();
		return;
	}

	/* This should also disable all related buttons. */
	m_ui->availableTagsView->clearSelection();
	m_ui->assignedTagsView->clearSelection();

	Json::MsgId1List jsonMsgIdList;
	for (qint64 msgId : m_msgIdList) {
		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);
	{
		QList<TagItem> assignedTagList = itemSetToList(assignedTagsUnion);
		m_assignedTagsModel.setTagList(assignedTagList);
		m_ui->removeAllButton->setEnabled(assignedTagList.count() > 0);
	}
}

void DlgTags::initDlg(bool enableTagAssignment)
{
	setIcons();

	m_ui->availableTagsView->setItemDelegate(&m_availableTagsDelegate);
	m_ui->availableTagsView->setModel(&m_availableTagsModel);
	m_ui->availableTagsView->setSelectionMode(QAbstractItemView::ExtendedSelection);
	m_ui->availableTagsView->setSelectionBehavior(QAbstractItemView::SelectRows);

	m_ui->assignedTagsView->setItemDelegate(&m_assignedTagsDelegate);
	m_ui->assignedTagsView->setModel(&m_assignedTagsModel);
	m_ui->assignedTagsView->setSelectionMode(QAbstractItemView::ExtendedSelection);
	m_ui->assignedTagsView->setSelectionBehavior(QAbstractItemView::SelectRows);

	{
		TableKeyPressFilter *filter =
		    new (::std::nothrow) TableKeyPressFilter(m_ui->availableTagsView);
		if (filter != Q_NULLPTR) {
#ifdef Q_OS_OSX
			filter->registerAction(Qt::CTRL | Qt::Key_Backspace,
			    deleteSelectedTagsViaFilter, this, true);
#endif /* Q_OS_OSX */
			filter->registerAction(Qt::Key_Delete,
			    deleteSelectedTagsViaFilter, this, true);
			filter->registerAction(Qt::Key_Return,
			    updateSelectedTagsViaFilter, this, true);
			filter->registerAction(Qt::Key_Enter, /* On keypad. */
			    updateSelectedTagsViaFilter, this, true);
			m_ui->availableTagsView->installEventFilter(filter);
		}
	}

	connect(m_ui->availableTagsView->selectionModel(),
	    SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this,
	    SLOT(handleAvailableSelectionChange()));

	connect(m_ui->assignedTagsView->selectionModel(),
	    SIGNAL(selectionChanged(QItemSelection, QItemSelection)), this,
	    SLOT(handleAssignedSelectionChange()));

	connect(m_ui->addTagButton, SIGNAL(clicked()), this, SLOT(addTag()));
	connect(m_ui->deleteTagButton, SIGNAL(clicked()), this,
	    SLOT(deleteTag()));
	connect(m_ui->updateTagButton, SIGNAL(clicked()), this,
	    SLOT(updateTag()));

	databaseConnectActions();

	/* Message tag assignment is going to be edited. */
	if (enableTagAssignment) {
		connect(m_ui->assignButton, SIGNAL(clicked()), this,
		    SLOT(assignSelectedTagsToMsgs()));
		connect(m_ui->removeButton, SIGNAL(clicked()), this,
		    SLOT(removeSelectedTagsFromMsgs()));
		connect(m_ui->removeAllButton, SIGNAL(clicked()), this,
		    SLOT(removeAllTagsFromMsgs()));
	}

	m_ui->deleteTagButton->setEnabled(false);
	m_ui->updateTagButton->setEnabled(false);

	m_ui->assignButton->setEnabled(false);
	m_ui->assignButton->setVisible(enableTagAssignment);
	m_ui->removeButton->setEnabled(false);
	m_ui->removeButton->setVisible(enableTagAssignment);

	m_ui->assignedGroup->setEnabled(enableTagAssignment);
	m_ui->assignedGroup->setVisible(enableTagAssignment);
}

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

	loadAssignments();
}

void DlgTags::clearModels(void)
{
	/* Clear all stuff. */
	m_availableTagsModel.setTagList(Json::TagEntryList());
	m_assignedTagsModel.setTagList(Json::TagEntryList());
}

void DlgTags::databaseConnectActions(void)
{
	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()));
}

void DlgTags::selectAllAssingedTagsFromMsgs(void)
{
	int rows = m_ui->availableTagsView->model()->rowCount();
	for (int i = 0; i < rows; ++i) {
		const QModelIndex idx = m_ui->availableTagsView->model()->index(i, 0);
		const qint64 id = getTagIdFromIndex(idx);
		for (const Json::MsgId1 &msgId : m_msgTagItemsMap.keys()) {
			const TagItemList tags = m_msgTagItemsMap[msgId];
			for (const TagItem &tag : tags) {
				if (tag.id() == id) {
					m_ui->availableTagsView->selectionModel()->select(
					    idx, QItemSelectionModel::Select);
				}
			}
		}
	}
}

void DlgTags::setIcons(void)
{
	if (GuiStyles::intToAppUiTheme(PrefsSpecific::appUiTheme(*GlobInstcs::prefsPtr)) == GuiStyles::NO_THEME) {
		m_ui->assignButton->setIcon(IconContainer::construcIcon(IconContainer::ICON_APP_ARROW_RIGHT));
		m_ui->removeButton->setIcon(IconContainer::construcIcon(IconContainer::ICON_APP_ARROW_LEFT));
		m_ui->removeAllButton->setIcon(IconContainer::construcIcon(IconContainer::ICON_APP_ARROW_LEFT));
	} else {
		m_ui->assignButton->setIcon(IconContainer::construcIcon(IconContainer::ICON_APP_ARROW_RIGHT_WHITE));
		m_ui->removeButton->setIcon(IconContainer::construcIcon(IconContainer::ICON_APP_ARROW_LEFT_WHITE));
		m_ui->removeAllButton->setIcon(IconContainer::construcIcon(IconContainer::ICON_APP_ARROW_LEFT_WHITE));
	}
}

void DlgTags::deleteSelectedTagsViaFilter(QObject *dlgPtr)
{
	if (Q_UNLIKELY(dlgPtr == Q_NULLPTR)) {
		return;
	}

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

	if (Q_UNLIKELY(!dlg->m_ui->deleteTagButton->isEnabled())) {
		return;
	}

	dlg->m_ui->deleteTagButton->click();
}

void DlgTags::updateSelectedTagsViaFilter(QObject *dlgPtr){
	if (Q_UNLIKELY(dlgPtr == Q_NULLPTR)) {
		return;
	}

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

	if (Q_UNLIKELY(!dlg->m_ui->updateTagButton->isEnabled())) {
		return;
	}

	dlg->m_ui->updateTagButton->click();
}
