// Copyright 2013-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "UserListModel.h"

#include "Channel.h"
#include "QtUtils.h"
#include "Utils.h"

#include <algorithm>

#ifdef _MSC_VER
#	include <functional>
#endif

#include <vector>

UserListModel::UserListModel(const MumbleProto::UserList &userList, QObject *parent_)
	: QAbstractTableModel(parent_), m_legacyMode(false) {
	m_userList.reserve(userList.users_size());
	for (int i = 0; i < userList.users_size(); ++i) {
		m_userList.append(userList.users(i));
	}

	if (!m_userList.empty()) {
		const MumbleProto::UserList_User &user = m_userList.back();
		m_legacyMode                           = !user.has_last_seen() || !user.has_last_channel();
	}
}

int UserListModel::rowCount(const QModelIndex &parentIndex) const {
	if (parentIndex.isValid())
		return 0;

	return m_userList.size();
}

int UserListModel::columnCount(const QModelIndex &parentIndex) const {
	if (parentIndex.isValid())
		return 0;

	if (m_legacyMode) {
		// Only COL_NICK
		return 1;
	}

	return COUNT_COL;
}

QVariant UserListModel::headerData(int section, Qt::Orientation orientation, int role) const {
	if (orientation != Qt::Horizontal)
		return QVariant();

	if (section < 0 || section >= columnCount())
		return QVariant();

	if (role == Qt::DisplayRole) {
		switch (section) {
			case COL_NICK:
				return tr("Nick");
			case COL_INACTIVEDAYS:
				return tr("Inactive days");
			case COL_LASTCHANNEL:
				return tr("Last channel");
			default:
				return QVariant();
		}
	}

	return QVariant();
}

QVariant UserListModel::data(const QModelIndex &dataIndex, int role) const {
	if (!dataIndex.isValid())
		return QVariant();

	if (dataIndex.row() < 0 || dataIndex.row() >= m_userList.size())
		return QVariant();

	if (dataIndex.column() >= columnCount())
		return QVariant();

	const MumbleProto::UserList_User &user = m_userList[dataIndex.row()];

	if (role == Qt::DisplayRole) {
		switch (dataIndex.column()) {
			case COL_NICK:
				return u8(user.name());
			case COL_INACTIVEDAYS:
				return lastSeenToTodayDayCount(user.last_seen());
			case COL_LASTCHANNEL:
				return pathForChannelId(user.last_channel());
			default:
				return QVariant();
		}
	} else if (role == Qt::ToolTipRole) {
		switch (dataIndex.column()) {
			case COL_INACTIVEDAYS:
				return tr("Last seen: %1")
					.arg(user.last_seen().empty() ? tr("Never") : u8(user.last_seen()).toHtmlEscaped());
			case COL_LASTCHANNEL:
				return tr("Channel ID: %1").arg(user.last_channel());
			default:
				return QVariant();
		}
	} else if (role == Qt::UserRole) {
		switch (dataIndex.column()) {
			case COL_INACTIVEDAYS:
				return isoUTCToDateTime(user.last_seen());
			case COL_LASTCHANNEL:
				return user.last_channel();
			default:
				return QVariant();
		}
	} else if (role == Qt::EditRole) {
		if (dataIndex.column() == COL_NICK) {
			return u8(user.name());
		}
	}

	return QVariant();
}

bool UserListModel::setData(const QModelIndex &dataIndex, const QVariant &value, int role) {
	if (!dataIndex.isValid())
		return false;

	if (dataIndex.column() != COL_NICK || role != Qt::EditRole)
		return false;

	if (dataIndex.row() < 0 || dataIndex.row() >= m_userList.size())
		return false;

	const std::string newNick = u8(value.toString());
	if (newNick.empty()) {
		// Empty nick is not valid
		return false;
	}

	MumbleProto::UserList_User &user = m_userList[dataIndex.row()];
	if (newNick != user.name()) {
		foreach (const MumbleProto::UserList_User &otherUser, m_userList) {
			if (otherUser.name() == newNick) {
				// Duplicate is not valid
				return false;
			}
		}

		user.set_name(newNick);
		m_changes[user.user_id()] = user;

		emit dataChanged(dataIndex, dataIndex);
	}

	return true;
}

Qt::ItemFlags UserListModel::flags(const QModelIndex &flagIndex) const {
	const Qt::ItemFlags original = QAbstractTableModel::flags(flagIndex);

	if (flagIndex.column() == COL_NICK) {
		return original | Qt::ItemIsEditable;
	}

	return original;
}

bool UserListModel::removeRows(int row, int count, const QModelIndex &parentIndex) {
	if (row + count > m_userList.size())
		return false;

	beginRemoveRows(parentIndex, row, row + count - 1);

	ModelUserList::Iterator startIt = m_userList.begin() + row;
	ModelUserList::Iterator endIt   = startIt + count;

	for (ModelUserList::Iterator it = startIt; it != endIt; ++it) {
		it->clear_name();
		m_changes[it->user_id()] = *it;
	}

	m_userList.erase(startIt, endIt);

	endRemoveRows();
	return true;
}

void UserListModel::removeRowsInSelection(const QItemSelection &selection) {
	QModelIndexList indices = selection.indexes();

	std::vector< int > rows;
	rows.reserve(static_cast< std::size_t >(indices.size()));

	foreach (const QModelIndex &idx, indices) {
		if (idx.column() != COL_NICK)
			continue;

		rows.push_back(idx.row());
	}

	// Make sure to presort the rows so we work from back (high) to front (low).
	// This prevents the row numbers from becoming invalid after removing a group.
	// The basic idea is to take a number of sorted rows (e.g. 10,9,5,3,2,1) and
	// delete them with the minimum number of removeRows calls. This means grouping
	// adjacent rows (e.g. (10,9),(5),(3,2,1)) and using a removeRows call for each group.
	std::sort(rows.begin(), rows.end(), std::greater< int >());

	int nextRow       = -2;
	int groupRowCount = 0;

	for (size_t i = 0; i < rows.size(); ++i) {
		if (rows[i] == nextRow) {
			++groupRowCount;
			--nextRow;
		} else {
			if (groupRowCount > 0) {
				// Remove previous group
				const int lastRowInGroup = nextRow + 1;
				removeRows(lastRowInGroup, groupRowCount);
			}

			// Start next group
			nextRow       = rows[i] - 1;
			groupRowCount = 1;
		}
	}

	if (groupRowCount > 0) {
		// Remove leftover group
		const int lastRowInGroup = nextRow + 1;
		removeRows(lastRowInGroup, groupRowCount);
	}
}

MumbleProto::UserList UserListModel::getUserListUpdate() const {
	MumbleProto::UserList updateList;

	for (ModelUserListChangeMap::ConstIterator it = m_changes.constBegin(); it != m_changes.constEnd(); ++it) {
		MumbleProto::UserList_User *user = updateList.add_users();
		*user                            = it.value();
	}

	return updateList;
}

bool UserListModel::isUserListDirty() const {
	return !m_changes.empty();
}

bool UserListModel::isLegacy() const {
	return m_legacyMode;
}

QVariant UserListModel::lastSeenToTodayDayCount(const std::string &lastSeenDate) const {
	if (lastSeenDate.empty())
		return QVariant();

	QVariant count = m_stringToLastSeenToTodayCount.value(u8(lastSeenDate));
	if (count.isNull()) {
		QDateTime dt = isoUTCToDateTime(lastSeenDate);
		if (!dt.isValid()) {
			// Not convertible to int
			return QVariant();
		}
		count = dt.daysTo(QDateTime::currentDateTime().toUTC());
		m_stringToLastSeenToTodayCount.insert(u8(lastSeenDate), count);
	}
	return count;
}

QString UserListModel::pathForChannelId(const unsigned int channelId) const {
	QString path = m_channelIdToPathMap.value(channelId);
	if (path.isNull()) {
		path = QLatin1String("-");

		Channel *channel = Channel::get(channelId);
		if (channel) {
			QStringList pathParts;

			while (channel->cParent) {
				pathParts.prepend(channel->qsName);
				channel = channel->cParent;
			}

			pathParts.append(QString());

			path = QLatin1String("/ ") + pathParts.join(QLatin1String(" / "));
		}

		m_channelIdToPathMap.insert(channelId, path);
	}
	return path;
}

QDateTime UserListModel::isoUTCToDateTime(const std::string &isoTime) const {
	QDateTime dt = QDateTime::fromString(u8(isoTime), Qt::ISODate);
	dt.setTimeSpec(Qt::UTC);
	return dt;
}
