// SPDX-FileCopyrightText: 2021 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later

#include "RoomlistModel.h"

#include "Cache_p.h"
#include "ChatPage.h"
#include "MatrixClient.h"
#include "MxcImageProvider.h"
#include "TimelineModel.h"
#include "TimelineViewManager.h"
#include "UserSettingsPage.h"

RoomlistModel::RoomlistModel(TimelineViewManager *parent)
  : QAbstractListModel(parent)
  , manager(parent)
{
        [[maybe_unused]] static auto id = qRegisterMetaType<RoomPreview>();

        connect(ChatPage::instance(), &ChatPage::decryptSidebarChanged, this, [this]() {
                auto decrypt = ChatPage::instance()->userSettings()->decryptSidebar();
                QHash<QString, QSharedPointer<TimelineModel>>::iterator i;
                for (i = models.begin(); i != models.end(); ++i) {
                        auto ptr = i.value();

                        if (!ptr.isNull()) {
                                ptr->setDecryptDescription(decrypt);
                                ptr->updateLastMessage();
                        }
                }
        });

        connect(this,
                &RoomlistModel::totalUnreadMessageCountUpdated,
                ChatPage::instance(),
                &ChatPage::unreadMessages);

        connect(
          this,
          &RoomlistModel::fetchedPreview,
          this,
          [this](QString roomid, RoomInfo info) {
                  if (this->previewedRooms.contains(roomid)) {
                          this->previewedRooms.insert(roomid, std::move(info));
                          auto idx = this->roomidToIndex(roomid);
                          emit dataChanged(index(idx),
                                           index(idx),
                                           {
                                             Roles::RoomName,
                                             Roles::AvatarUrl,
                                             Roles::IsSpace,
                                             Roles::IsPreviewFetched,
                                             Qt::DisplayRole,
                                           });
                  }
          },
          Qt::QueuedConnection);
}

QHash<int, QByteArray>
RoomlistModel::roleNames() const
{
        return {
          {AvatarUrl, "avatarUrl"},
          {RoomName, "roomName"},
          {RoomId, "roomId"},
          {LastMessage, "lastMessage"},
          {Time, "time"},
          {Timestamp, "timestamp"},
          {HasUnreadMessages, "hasUnreadMessages"},
          {HasLoudNotification, "hasLoudNotification"},
          {NotificationCount, "notificationCount"},
          {IsInvite, "isInvite"},
          {IsSpace, "isSpace"},
          {Tags, "tags"},
          {ParentSpaces, "parentSpaces"},
        };
}

QVariant
RoomlistModel::data(const QModelIndex &index, int role) const
{
        if (index.row() >= 0 && static_cast<size_t>(index.row()) < roomids.size()) {
                auto roomid = roomids.at(index.row());

                if (role == Roles::ParentSpaces) {
                        auto parents = cache::client()->getParentRoomIds(roomid.toStdString());
                        QStringList list;
                        for (const auto &t : parents)
                                list.push_back(QString::fromStdString(t));
                        return list;
                } else if (role == Roles::RoomId) {
                        return roomid;
                }

                if (models.contains(roomid)) {
                        auto room = models.value(roomid);
                        switch (role) {
                        case Roles::AvatarUrl:
                                return room->roomAvatarUrl();
                        case Roles::RoomName:
                                return room->plainRoomName();
                        case Roles::LastMessage:
                                return room->lastMessage().body;
                        case Roles::Time:
                                return room->lastMessage().descriptiveTime;
                        case Roles::Timestamp:
                                return QVariant(
                                  static_cast<quint64>(room->lastMessage().timestamp));
                        case Roles::HasUnreadMessages:
                                return this->roomReadStatus.count(roomid) &&
                                       this->roomReadStatus.at(roomid);
                        case Roles::HasLoudNotification:
                                return room->hasMentions();
                        case Roles::NotificationCount:
                                return room->notificationCount();
                        case Roles::IsInvite:
                                return false;
                        case Roles::IsSpace:
                                return room->isSpace();
                        case Roles::IsPreview:
                                return false;
                        case Roles::Tags: {
                                auto info = cache::singleRoomInfo(roomid.toStdString());
                                QStringList list;
                                for (const auto &t : info.tags)
                                        list.push_back(QString::fromStdString(t));
                                return list;
                        }
                        default:
                                return {};
                        }
                } else if (invites.contains(roomid)) {
                        auto room = invites.value(roomid);
                        switch (role) {
                        case Roles::AvatarUrl:
                                return QString::fromStdString(room.avatar_url);
                        case Roles::RoomName:
                                return QString::fromStdString(room.name);
                        case Roles::LastMessage:
                                return tr("Pending invite.");
                        case Roles::Time:
                                return QString();
                        case Roles::Timestamp:
                                return QVariant(static_cast<quint64>(0));
                        case Roles::HasUnreadMessages:
                        case Roles::HasLoudNotification:
                                return false;
                        case Roles::NotificationCount:
                                return 0;
                        case Roles::IsInvite:
                                return true;
                        case Roles::IsSpace:
                                return false;
                        case Roles::IsPreview:
                                return false;
                        case Roles::Tags:
                                return QStringList();
                        default:
                                return {};
                        }
                } else if (previewedRooms.contains(roomid) &&
                           previewedRooms.value(roomid).has_value()) {
                        auto room = previewedRooms.value(roomid).value();
                        switch (role) {
                        case Roles::AvatarUrl:
                                return QString::fromStdString(room.avatar_url);
                        case Roles::RoomName:
                                return QString::fromStdString(room.name);
                        case Roles::LastMessage:
                                return tr("Previewing this room");
                        case Roles::Time:
                                return QString();
                        case Roles::Timestamp:
                                return QVariant(static_cast<quint64>(0));
                        case Roles::HasUnreadMessages:
                        case Roles::HasLoudNotification:
                                return false;
                        case Roles::NotificationCount:
                                return 0;
                        case Roles::IsInvite:
                                return false;
                        case Roles::IsSpace:
                                return room.is_space;
                        case Roles::IsPreview:
                                return true;
                        case Roles::IsPreviewFetched:
                                return true;
                        case Roles::Tags:
                                return QStringList();
                        default:
                                return {};
                        }
                } else {
                        if (role == Roles::IsPreview)
                                return true;
                        else if (role == Roles::IsPreviewFetched)
                                return false;

                        fetchPreview(roomid);
                        switch (role) {
                        case Roles::AvatarUrl:
                                return QString();
                        case Roles::RoomName:
                                return tr("No preview available");
                        case Roles::LastMessage:
                                return QString();
                        case Roles::Time:
                                return QString();
                        case Roles::Timestamp:
                                return QVariant(static_cast<quint64>(0));
                        case Roles::HasUnreadMessages:
                        case Roles::HasLoudNotification:
                                return false;
                        case Roles::NotificationCount:
                                return 0;
                        case Roles::IsInvite:
                                return false;
                        case Roles::IsSpace:
                                return false;
                        case Roles::Tags:
                                return QStringList();
                        default:
                                return {};
                        }
                }
        } else {
                return {};
        }
}

void
RoomlistModel::updateReadStatus(const std::map<QString, bool> roomReadStatus_)
{
        std::vector<int> roomsToUpdate;
        roomsToUpdate.resize(roomReadStatus_.size());
        for (const auto &[roomid, roomUnread] : roomReadStatus_) {
                if (roomUnread != roomReadStatus[roomid]) {
                        roomsToUpdate.push_back(this->roomidToIndex(roomid));
                }

                this->roomReadStatus[roomid] = roomUnread;
        }

        for (auto idx : roomsToUpdate) {
                emit dataChanged(index(idx),
                                 index(idx),
                                 {
                                   Roles::HasUnreadMessages,
                                 });
        }
}
void
RoomlistModel::addRoom(const QString &room_id, bool suppressInsertNotification)
{
        if (!models.contains(room_id)) {
                // ensure we get read status updates and are only connected once
                connect(cache::client(),
                        &Cache::roomReadStatus,
                        this,
                        &RoomlistModel::updateReadStatus,
                        Qt::UniqueConnection);

                QSharedPointer<TimelineModel> newRoom(new TimelineModel(manager, room_id));
                newRoom->setDecryptDescription(
                  ChatPage::instance()->userSettings()->decryptSidebar());

                connect(newRoom.data(),
                        &TimelineModel::newEncryptedImage,
                        manager->imageProvider(),
                        &MxcImageProvider::addEncryptionInfo);
                connect(newRoom.data(),
                        &TimelineModel::forwardToRoom,
                        manager,
                        &TimelineViewManager::forwardMessageToRoom);
                connect(
                  newRoom.data(), &TimelineModel::lastMessageChanged, this, [room_id, this]() {
                          auto idx = this->roomidToIndex(room_id);
                          emit dataChanged(index(idx),
                                           index(idx),
                                           {
                                             Roles::HasLoudNotification,
                                             Roles::LastMessage,
                                             Roles::Timestamp,
                                             Roles::NotificationCount,
                                             Qt::DisplayRole,
                                           });
                  });
                connect(
                  newRoom.data(), &TimelineModel::roomAvatarUrlChanged, this, [room_id, this]() {
                          auto idx = this->roomidToIndex(room_id);
                          emit dataChanged(index(idx),
                                           index(idx),
                                           {
                                             Roles::AvatarUrl,
                                           });
                  });
                connect(newRoom.data(), &TimelineModel::roomNameChanged, this, [room_id, this]() {
                        auto idx = this->roomidToIndex(room_id);
                        emit dataChanged(index(idx),
                                         index(idx),
                                         {
                                           Roles::RoomName,
                                         });
                });
                connect(
                  newRoom.data(), &TimelineModel::notificationsChanged, this, [room_id, this]() {
                          auto idx = this->roomidToIndex(room_id);
                          emit dataChanged(index(idx),
                                           index(idx),
                                           {
                                             Roles::HasLoudNotification,
                                             Roles::NotificationCount,
                                             Qt::DisplayRole,
                                           });

                          int total_unread_msgs = 0;

                          for (const auto &room : models) {
                                  if (!room.isNull())
                                          total_unread_msgs += room->notificationCount();
                          }

                          emit totalUnreadMessageCountUpdated(total_unread_msgs);
                  });

                newRoom->updateLastMessage();

                std::vector<QString> previewsToAdd;
                if (newRoom->isSpace()) {
                        auto childs = cache::client()->getChildRoomIds(room_id.toStdString());
                        for (const auto &c : childs) {
                                auto id = QString::fromStdString(c);
                                if (!(models.contains(id) || invites.contains(id) ||
                                      previewedRooms.contains(id))) {
                                        previewsToAdd.push_back(std::move(id));
                                }
                        }
                }

                bool wasInvite  = invites.contains(room_id);
                bool wasPreview = previewedRooms.contains(room_id);
                if (!suppressInsertNotification &&
                    ((!wasInvite && !wasPreview) || !previewedRooms.empty()))
                        // if the old room was already in the list, don't add it. Also add all
                        // previews at the same time.
                        beginInsertRows(QModelIndex(),
                                        (int)roomids.size(),
                                        (int)(roomids.size() + previewsToAdd.size() -
                                              ((wasInvite || wasPreview) ? 1 : 0)));

                models.insert(room_id, std::move(newRoom));
                if (wasInvite) {
                        auto idx = roomidToIndex(room_id);
                        invites.remove(room_id);
                        emit dataChanged(index(idx), index(idx));
                } else if (wasPreview) {
                        auto idx = roomidToIndex(room_id);
                        previewedRooms.remove(room_id);
                        emit dataChanged(index(idx), index(idx));
                } else {
                        roomids.push_back(room_id);
                }

                for (auto p : previewsToAdd) {
                        previewedRooms.insert(p, std::nullopt);
                        roomids.push_back(std::move(p));
                }

                if (!suppressInsertNotification &&
                    ((!wasInvite && !wasPreview) || !previewedRooms.empty()))
                        endInsertRows();
        }
}

void
RoomlistModel::fetchPreview(QString roomid_) const
{
        std::string roomid = roomid_.toStdString();
        http::client()->get_state_event<mtx::events::state::Create>(
          roomid,
          "",
          [this, roomid](const mtx::events::state::Create &c, mtx::http::RequestErr err) {
                  bool is_space = false;
                  if (!err) {
                          is_space = c.type == mtx::events::state::room_type::space;
                  }

                  http::client()->get_state_event<mtx::events::state::Avatar>(
                    roomid,
                    "",
                    [this, roomid, is_space](const mtx::events::state::Avatar &a,
                                             mtx::http::RequestErr) {
                            auto avatar_url = a.url;

                            http::client()->get_state_event<mtx::events::state::Topic>(
                              roomid,
                              "",
                              [this, roomid, avatar_url, is_space](
                                const mtx::events::state::Topic &t, mtx::http::RequestErr) {
                                      auto topic = t.topic;
                                      http::client()->get_state_event<mtx::events::state::Name>(
                                        roomid,
                                        "",
                                        [this, roomid, topic, avatar_url, is_space](
                                          const mtx::events::state::Name &n,
                                          mtx::http::RequestErr err) {
                                                if (err) {
                                                        nhlog::net()->warn(
                                                          "Failed to fetch name event to "
                                                          "create preview for {}",
                                                          roomid);
                                                }

                                                // don't even add a preview, if we got not a single
                                                // response
                                                if (n.name.empty() && avatar_url.empty() &&
                                                    topic.empty())
                                                        return;

                                                RoomInfo info{};
                                                info.name       = n.name;
                                                info.is_space   = is_space;
                                                info.avatar_url = avatar_url;
                                                info.topic      = topic;

                                                const_cast<RoomlistModel *>(this)->fetchedPreview(
                                                  QString::fromStdString(roomid), info);
                                        });
                              });
                    });
          });
}

void
RoomlistModel::sync(const mtx::responses::Rooms &rooms)
{
        for (const auto &[room_id, room] : rooms.join) {
                auto qroomid = QString::fromStdString(room_id);

                // addRoom will only add the room, if it doesn't exist
                addRoom(qroomid);
                const auto &room_model = models.value(qroomid);
                room_model->sync(room);
                // room_model->addEvents(room.timeline);
                connect(room_model.data(),
                        &TimelineModel::newCallEvent,
                        manager->callManager(),
                        &CallManager::syncEvent,
                        Qt::UniqueConnection);

                if (ChatPage::instance()->userSettings()->typingNotifications()) {
                        for (const auto &ev : room.ephemeral.events) {
                                if (auto t = std::get_if<
                                      mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>(
                                      &ev)) {
                                        std::vector<QString> typing;
                                        typing.reserve(t->content.user_ids.size());
                                        for (const auto &user : t->content.user_ids) {
                                                if (user != http::client()->user_id().to_string())
                                                        typing.push_back(
                                                          QString::fromStdString(user));
                                        }
                                        room_model->updateTypingUsers(typing);
                                }
                        }
                }
        }

        for (const auto &[room_id, room] : rooms.leave) {
                (void)room;
                auto qroomid = QString::fromStdString(room_id);

                if ((currentRoom_ && currentRoom_->roomId() == qroomid) ||
                    (currentRoomPreview_ && currentRoomPreview_->roomid() == qroomid))
                        resetCurrentRoom();

                auto idx = this->roomidToIndex(qroomid);
                if (idx != -1) {
                        beginRemoveRows(QModelIndex(), idx, idx);
                        roomids.erase(roomids.begin() + idx);
                        if (models.contains(qroomid))
                                models.remove(qroomid);
                        else if (invites.contains(qroomid))
                                invites.remove(qroomid);
                        endRemoveRows();
                }
        }

        for (const auto &[room_id, room] : rooms.invite) {
                (void)room;
                auto qroomid = QString::fromStdString(room_id);

                auto invite = cache::client()->invite(room_id);
                if (!invite)
                        continue;

                if (invites.contains(qroomid)) {
                        invites[qroomid] = *invite;
                        auto idx         = roomidToIndex(qroomid);
                        emit dataChanged(index(idx), index(idx));
                } else {
                        beginInsertRows(QModelIndex(), (int)roomids.size(), (int)roomids.size());
                        invites.insert(qroomid, *invite);
                        roomids.push_back(std::move(qroomid));
                        endInsertRows();
                }
        }
}

void
RoomlistModel::initializeRooms()
{
        beginResetModel();
        models.clear();
        roomids.clear();
        invites.clear();
        currentRoom_ = nullptr;

        invites = cache::client()->invites();
        for (const auto &id : invites.keys())
                roomids.push_back(id);

        for (const auto &id : cache::client()->roomIds())
                addRoom(id, true);

        endResetModel();
}

void
RoomlistModel::clear()
{
        beginResetModel();
        models.clear();
        invites.clear();
        roomids.clear();
        currentRoom_ = nullptr;
        emit currentRoomChanged();
        endResetModel();
}

void
RoomlistModel::joinPreview(QString roomid, QString parentSpace)
{
        if (previewedRooms.contains(roomid)) {
                auto child = cache::client()->getStateEvent<mtx::events::state::space::Child>(
                  parentSpace.toStdString(), roomid.toStdString());
                ChatPage::instance()->joinRoomVia(roomid.toStdString(),
                                                  (child && child->content.via)
                                                    ? child->content.via.value()
                                                    : std::vector<std::string>{},
                                                  false);
        }
}
void
RoomlistModel::acceptInvite(QString roomid)
{
        if (invites.contains(roomid)) {
                auto idx = roomidToIndex(roomid);

                if (idx != -1) {
                        beginRemoveRows(QModelIndex(), idx, idx);
                        roomids.erase(roomids.begin() + idx);
                        invites.remove(roomid);
                        endRemoveRows();
                        ChatPage::instance()->joinRoom(roomid);
                }
        }
}
void
RoomlistModel::declineInvite(QString roomid)
{
        if (invites.contains(roomid)) {
                auto idx = roomidToIndex(roomid);

                if (idx != -1) {
                        beginRemoveRows(QModelIndex(), idx, idx);
                        roomids.erase(roomids.begin() + idx);
                        invites.remove(roomid);
                        endRemoveRows();
                        ChatPage::instance()->leaveRoom(roomid);
                }
        }
}
void
RoomlistModel::leave(QString roomid)
{
        if (models.contains(roomid)) {
                auto idx = roomidToIndex(roomid);

                if (idx != -1) {
                        beginRemoveRows(QModelIndex(), idx, idx);
                        roomids.erase(roomids.begin() + idx);
                        models.remove(roomid);
                        endRemoveRows();
                        ChatPage::instance()->leaveRoom(roomid);
                }
        }
}

void
RoomlistModel::setCurrentRoom(QString roomid)
{
        nhlog::ui()->debug("Trying to switch to: {}", roomid.toStdString());
        if (models.contains(roomid)) {
                currentRoom_ = models.value(roomid);
                currentRoomPreview_.reset();
                emit currentRoomChanged();
                nhlog::ui()->debug("Switched to: {}", roomid.toStdString());
        } else if (invites.contains(roomid) || previewedRooms.contains(roomid)) {
                currentRoom_ = nullptr;
                std::optional<RoomInfo> i;

                RoomPreview p;

                if (invites.contains(roomid)) {
                        i           = invites.value(roomid);
                        p.isInvite_ = true;
                } else {
                        i           = previewedRooms.value(roomid);
                        p.isInvite_ = false;
                }

                if (i) {
                        p.roomid_           = roomid;
                        p.roomName_         = QString::fromStdString(i->name);
                        p.roomTopic_        = QString::fromStdString(i->topic);
                        p.roomAvatarUrl_    = QString::fromStdString(i->avatar_url);
                        currentRoomPreview_ = std::move(p);
                }

                emit currentRoomChanged();
                nhlog::ui()->debug("Switched to: {}", roomid.toStdString());
        }
}

namespace {
enum NotificationImportance : short
{
        ImportanceDisabled = -3,
        NoPreview          = -2,
        Preview            = -1,
        AllEventsRead      = 0,
        NewMessage         = 1,
        NewMentions        = 2,
        Invite             = 3,
        SubSpace           = 4,
        CurrentSpace       = 5,
};
}

short int
FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const
{
        // Returns the degree of importance of the unread messages in the room.
        // If sorting by importance is disabled in settings, this only ever
        // returns ImportanceDisabled or Invite
        if (sourceModel()->data(idx, RoomlistModel::IsSpace).toBool()) {
                if (filterType == FilterBy::Space &&
                    filterStr == sourceModel()->data(idx, RoomlistModel::RoomId).toString())
                        return CurrentSpace;
                else
                        return SubSpace;
        } else if (sourceModel()->data(idx, RoomlistModel::IsPreview).toBool()) {
                if (sourceModel()->data(idx, RoomlistModel::IsPreviewFetched).toBool())
                        return Preview;
                else
                        return NoPreview;
        } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) {
                return Invite;
        } else if (!this->sortByImportance) {
                return ImportanceDisabled;
        } else if (sourceModel()->data(idx, RoomlistModel::HasLoudNotification).toBool()) {
                return NewMentions;
        } else if (sourceModel()->data(idx, RoomlistModel::NotificationCount).toInt() > 0) {
                return NewMessage;
        } else {
                return AllEventsRead;
        }
}

bool
FilteredRoomlistModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
        QModelIndex const left_idx  = sourceModel()->index(left.row(), 0, QModelIndex());
        QModelIndex const right_idx = sourceModel()->index(right.row(), 0, QModelIndex());

        // Sort by "importance" (i.e. invites before mentions before
        // notifs before new events before old events), then secondly
        // by recency.

        // Checking importance first
        const auto a_importance = calculateImportance(left_idx);
        const auto b_importance = calculateImportance(right_idx);
        if (a_importance != b_importance) {
                return a_importance > b_importance;
        }

        // Now sort by recency
        // Zero if empty, otherwise the time that the event occured
        uint64_t a_recency = sourceModel()->data(left_idx, RoomlistModel::Timestamp).toULongLong();
        uint64_t b_recency = sourceModel()->data(right_idx, RoomlistModel::Timestamp).toULongLong();

        if (a_recency != b_recency)
                return a_recency > b_recency;
        else
                return left.row() < right.row();
}

FilteredRoomlistModel::FilteredRoomlistModel(RoomlistModel *model, QObject *parent)
  : QSortFilterProxyModel(parent)
  , roomlistmodel(model)
{
        this->sortByImportance = UserSettings::instance()->sortByImportance();
        setSourceModel(model);
        setDynamicSortFilter(true);

        QObject::connect(UserSettings::instance().get(),
                         &UserSettings::roomSortingChanged,
                         this,
                         [this](bool sortByImportance_) {
                                 this->sortByImportance = sortByImportance_;
                                 invalidate();
                         });

        connect(roomlistmodel,
                &RoomlistModel::currentRoomChanged,
                this,
                &FilteredRoomlistModel::currentRoomChanged);

        sort(0);
}

void
FilteredRoomlistModel::updateHiddenTagsAndSpaces()
{
        hiddenTags.clear();
        hiddenSpaces.clear();
        for (const auto &t : UserSettings::instance()->hiddenTags()) {
                if (t.startsWith("tag:"))
                        hiddenTags.push_back(t.mid(4));
                else if (t.startsWith("space:"))
                        hiddenSpaces.push_back(t.mid(6));
        }

        invalidateFilter();
}

bool
FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
{
        if (filterType == FilterBy::Nothing) {
                if (sourceModel()
                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview)
                      .toBool()) {
                        return false;
                }

                if (sourceModel()
                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
                      .toBool()) {
                        return false;
                }

                if (!hiddenTags.empty()) {
                        auto tags =
                          sourceModel()
                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
                            .toStringList();

                        for (const auto &t : tags)
                                if (hiddenTags.contains(t))
                                        return false;
                }

                if (!hiddenSpaces.empty()) {
                        auto parents =
                          sourceModel()
                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
                            .toStringList();
                        for (const auto &t : parents)
                                if (hiddenSpaces.contains(t))
                                        return false;
                }

                return true;
        } else if (filterType == FilterBy::Tag) {
                if (sourceModel()
                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsPreview)
                      .toBool()) {
                        return false;
                }

                if (sourceModel()
                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
                      .toBool()) {
                        return false;
                }

                auto tags = sourceModel()
                              ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
                              .toStringList();

                if (!tags.contains(filterStr))
                        return false;

                if (!hiddenTags.empty()) {
                        for (const auto &t : tags)
                                if (t != filterStr && hiddenTags.contains(t))
                                        return false;
                }

                if (!hiddenSpaces.empty()) {
                        auto parents =
                          sourceModel()
                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
                            .toStringList();
                        for (const auto &t : parents)
                                if (hiddenSpaces.contains(t))
                                        return false;
                }

                return true;
        } else if (filterType == FilterBy::Space) {
                if (filterStr == sourceModel()
                                   ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::RoomId)
                                   .toString())
                        return true;

                auto parents =
                  sourceModel()
                    ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
                    .toStringList();

                if (!parents.contains(filterStr))
                        return false;

                if (!hiddenTags.empty()) {
                        auto tags =
                          sourceModel()
                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
                            .toStringList();

                        for (const auto &t : tags)
                                if (hiddenTags.contains(t))
                                        return false;
                }

                if (!hiddenSpaces.empty()) {
                        for (const auto &t : parents)
                                if (hiddenSpaces.contains(t))
                                        return false;
                }

                if (sourceModel()
                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
                      .toBool() &&
                    !parents.contains(filterStr)) {
                        return false;
                }

                return true;
        } else {
                return true;
        }
}

void
FilteredRoomlistModel::toggleTag(QString roomid, QString tag, bool on)
{
        if (on) {
                http::client()->put_tag(
                  roomid.toStdString(), tag.toStdString(), {}, [tag](mtx::http::RequestErr err) {
                          if (err) {
                                  nhlog::ui()->error("Failed to add tag: {}, {}",
                                                     tag.toStdString(),
                                                     err->matrix_error.error);
                          }
                  });
        } else {
                http::client()->delete_tag(
                  roomid.toStdString(), tag.toStdString(), [tag](mtx::http::RequestErr err) {
                          if (err) {
                                  nhlog::ui()->error("Failed to delete tag: {}, {}",
                                                     tag.toStdString(),
                                                     err->matrix_error.error);
                          }
                  });
        }
}

void
FilteredRoomlistModel::nextRoom()
{
        auto r = currentRoom();

        if (r) {
                int idx = roomidToIndex(r->roomId());
                idx++;
                if (idx < rowCount()) {
                        setCurrentRoom(
                          data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
                }
        }
}

void
FilteredRoomlistModel::previousRoom()
{
        auto r = currentRoom();

        if (r) {
                int idx = roomidToIndex(r->roomId());
                idx--;
                if (idx >= 0) {
                        setCurrentRoom(
                          data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
                }
        }
}