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

#include "CommunitiesModel.h"

#include <mtx/responses/common.hpp>
#include <set>

#include "Cache.h"
#include "Cache_p.h"
#include "ChatPage.h"
#include "Logging.h"
#include "MatrixClient.h"
#include "Permissions.h"
#include "UserSettingsPage.h"
#include "Utils.h"
#include "timeline/TimelineModel.h"

CommunitiesModel::CommunitiesModel(QObject *parent)
  : QAbstractListModel(parent)
  , hiddenTagIds_{UserSettings::instance()->hiddenTags()}
  , mutedTagIds_{UserSettings::instance()->mutedTags()}
{
    instance_ = this;
}

QHash<int, QByteArray>
CommunitiesModel::roleNames() const
{
    return {
      {AvatarUrl, "avatarUrl"},
      {DisplayName, "displayName"},
      {Tooltip, "tooltip"},
      {Collapsed, "collapsed"},
      {Collapsible, "collapsible"},
      {Hidden, "hidden"},
      {Depth, "depth"},
      {Id, "id"},
      {UnreadMessages, "unreadMessages"},
      {HasLoudNotification, "hasLoudNotification"},
      {Muted, "muted"},
    };
}

bool
CommunitiesModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role != CommunitiesModel::Collapsed)
        return false;
    else if (index.row() >= 2 || index.row() - 2 < spaceOrder_.size()) {
        spaceOrder_.tree.at(index.row() - 2).collapsed = value.toBool();

        const auto cindex = spaceOrder_.lastChild(index.row() - 2);
        emit dataChanged(index, this->index(cindex + 2), {Collapsed, Qt::DisplayRole});
        spaceOrder_.storeCollapsed();
        return true;
    } else
        return false;
}

QVariant
CommunitiesModel::data(const QModelIndex &index, int role) const
{
    if (role == CommunitiesModel::Roles::Muted) {
        if (index.row() == 0)
            return mutedTagIds_.contains(QStringLiteral("global"));
        else
            return mutedTagIds_.contains(data(index, CommunitiesModel::Roles::Id).toString());
    }

    if (index.row() == 0) {
        switch (role) {
        case CommunitiesModel::Roles::AvatarUrl:
            return QStringLiteral(":/icons/icons/ui/world.svg");
        case CommunitiesModel::Roles::DisplayName:
            return tr("All rooms");
        case CommunitiesModel::Roles::Tooltip:
            return tr("Shows all rooms without filtering.");
        case CommunitiesModel::Roles::Collapsed:
            return false;
        case CommunitiesModel::Roles::Collapsible:
            return false;
        case CommunitiesModel::Roles::Hidden:
            return false;
        case CommunitiesModel::Roles::Parent:
            return "";
        case CommunitiesModel::Roles::Depth:
            return 0;
        case CommunitiesModel::Roles::Id:
            return "";
        case CommunitiesModel::Roles::UnreadMessages:
            return (int)globalUnreads.notification_count;
        case CommunitiesModel::Roles::HasLoudNotification:
            return globalUnreads.highlight_count > 0;
        }
    } else if (index.row() == 1) {
        switch (role) {
        case CommunitiesModel::Roles::AvatarUrl:
            return QStringLiteral(":/icons/icons/ui/people.svg");
        case CommunitiesModel::Roles::DisplayName:
            return tr("Direct Chats");
        case CommunitiesModel::Roles::Tooltip:
            return tr("Show direct chats.");
        case CommunitiesModel::Roles::Collapsed:
            return false;
        case CommunitiesModel::Roles::Collapsible:
            return false;
        case CommunitiesModel::Roles::Hidden:
            return hiddenTagIds_.contains(QStringLiteral("dm"));
        case CommunitiesModel::Roles::Parent:
            return "";
        case CommunitiesModel::Roles::Depth:
            return 0;
        case CommunitiesModel::Roles::Id:
            return "dm";
        case CommunitiesModel::Roles::UnreadMessages:
            return (int)dmUnreads.notification_count;
        case CommunitiesModel::Roles::HasLoudNotification:
            return dmUnreads.highlight_count > 0;
        }
    } else if (index.row() - 2 < spaceOrder_.size()) {
        auto id = spaceOrder_.tree.at(index.row() - 2).id;
        switch (role) {
        case CommunitiesModel::Roles::AvatarUrl:
            return QString::fromStdString(spaces_.at(id).avatar_url);
        case CommunitiesModel::Roles::DisplayName:
        case CommunitiesModel::Roles::Tooltip:
            return QString::fromStdString(spaces_.at(id).name);
        case CommunitiesModel::Roles::Collapsed:
            return spaceOrder_.tree.at(index.row() - 2).collapsed;
        case CommunitiesModel::Roles::Collapsible: {
            auto idx = index.row() - 2;
            return idx != spaceOrder_.lastChild(idx);
        }
        case CommunitiesModel::Roles::Hidden:
            return hiddenTagIds_.contains("space:" + id);
        case CommunitiesModel::Roles::Parent: {
            if (auto p = spaceOrder_.parent(index.row() - 2); p >= 0)
                return spaceOrder_.tree[p].id;

            return "";
        }
        case CommunitiesModel::Roles::Depth:
            return spaceOrder_.tree.at(index.row() - 2).depth;
        case CommunitiesModel::Roles::Id:
            return "space:" + id;
        case CommunitiesModel::Roles::UnreadMessages: {
            int count = 0;
            auto end  = spaceOrder_.lastChild(index.row() - 2);
            for (int i = index.row() - 2; i <= end; i++)
                count +=
                  static_cast<int>(spaceOrder_.tree[i].notificationCounts.notification_count);
            return count;
        }
        case CommunitiesModel::Roles::HasLoudNotification: {
            auto end = spaceOrder_.lastChild(index.row() - 2);
            for (int i = index.row() - 2; i <= end; i++)
                if (spaceOrder_.tree[i].notificationCounts.highlight_count > 0)
                    return true;
            return false;
        }
        }
    } else if (index.row() - 2 < tags_.size() + spaceOrder_.size()) {
        auto tag = tags_.at(index.row() - 2 - spaceOrder_.size());
        if (tag == QLatin1String("m.favourite")) {
            switch (role) {
            case CommunitiesModel::Roles::AvatarUrl:
                return QStringLiteral(":/icons/icons/ui/star.svg");
            case CommunitiesModel::Roles::DisplayName:
                return tr("Favourites");
            case CommunitiesModel::Roles::Tooltip:
                return tr("Rooms you have favourited.");
            }
        } else if (tag == QLatin1String("m.lowpriority")) {
            switch (role) {
            case CommunitiesModel::Roles::AvatarUrl:
                return QStringLiteral(":/icons/icons/ui/lowprio.svg");
            case CommunitiesModel::Roles::DisplayName:
                return tr("Low Priority");
            case CommunitiesModel::Roles::Tooltip:
                return tr("Rooms with low priority.");
            }
        } else if (tag == QLatin1String("m.server_notice")) {
            switch (role) {
            case CommunitiesModel::Roles::AvatarUrl:
                return QStringLiteral(":/icons/icons/ui/tag.svg");
            case CommunitiesModel::Roles::DisplayName:
                return tr("Server Notices");
            case CommunitiesModel::Roles::Tooltip:
                return tr("Messages from your server or administrator.");
            }
        } else {
            switch (role) {
            case CommunitiesModel::Roles::AvatarUrl:
                return QStringLiteral(":/icons/icons/ui/tag.svg");
            case CommunitiesModel::Roles::DisplayName:
            case CommunitiesModel::Roles::Tooltip:
                return tag.mid(2);
            }
        }

        switch (role) {
        case CommunitiesModel::Roles::Hidden:
            return hiddenTagIds_.contains("tag:" + tag);
        case CommunitiesModel::Roles::Collapsed:
            return true;
        case CommunitiesModel::Roles::Collapsible:
            return false;
        case CommunitiesModel::Roles::Parent:
            return "";
        case CommunitiesModel::Roles::Depth:
            return 0;
        case CommunitiesModel::Roles::Id:
            return "tag:" + tag;
        case CommunitiesModel::Roles::UnreadMessages:
            if (auto it = tagNotificationCache.find(tag); it != tagNotificationCache.end())
                return (int)it->second.notification_count;
            else
                return 0;
        case CommunitiesModel::Roles::HasLoudNotification:
            if (auto it = tagNotificationCache.find(tag); it != tagNotificationCache.end())
                return it->second.highlight_count > 0;
            else
                return 0;
        }
    }
    return QVariant();
}

namespace {
struct temptree
{
    std::map<std::string, temptree> children;

    void insert(const std::vector<std::string> &parents, const std::string &child)
    {
        temptree *t = this;
        for (const auto &e : parents)
            t = &t->children[e];
        t->children[child];
    }

    void flatten(CommunitiesModel::FlatTree &to, int i = 0) const
    {
        for (const auto &[child, subtree] : children) {
            to.tree.push_back({QString::fromStdString(child), i, {}, false});
            subtree.flatten(to, i + 1);
        }
    }
};

void
addChildren(temptree &t,
            std::vector<std::string> path,
            std::string child,
            const std::map<std::string, std::set<std::string>> &children)
{
    if (std::find(path.begin(), path.end(), child) != path.end())
        return;

    path.push_back(child);

    if (children.count(child)) {
        for (const auto &c : children.at(child)) {
            t.insert(path, c);
            addChildren(t, path, c, children);
        }
    }
}
}

void
CommunitiesModel::initializeSidebar()
{
    beginResetModel();
    tags_.clear();
    spaceOrder_.tree.clear();
    spaces_.clear();
    tagNotificationCache.clear();
    globalUnreads.notification_count = {};
    dmUnreads.notification_count     = {};

    {
        auto e = cache::client()->getAccountData(mtx::events::EventType::Direct);
        if (e) {
            if (auto event =
                  std::get_if<mtx::events::AccountDataEvent<mtx::events::account_data::Direct>>(
                    &e.value())) {
                directMessages_.clear();
                for (const auto &[userId, roomIds] : event->content.user_to_rooms)
                    for (const auto &roomId : roomIds)
                        directMessages_.push_back(roomId);
            }
        }
    }

    std::set<std::string> ts;

    std::set<std::string> isSpace;
    std::map<std::string, std::set<std::string>> spaceChilds;
    std::map<std::string, std::set<std::string>> spaceParents;

    auto infos = cache::roomInfo();
    for (auto it = infos.begin(); it != infos.end(); ++it) {
        if (it.value().is_space) {
            spaces_[it.key()] = it.value();
            isSpace.insert(it.key().toStdString());
        } else {
            for (const auto &t : it.value().tags) {
                if (t.find("u.") == 0 || t.find("m." == 0)) {
                    ts.insert(t);
                }
            }
        }

        for (const auto &t : it->tags) {
            auto tagId = QString::fromStdString(t);
            auto &tNs  = tagNotificationCache[tagId];
            tNs.notification_count += it->notification_count;
            tNs.highlight_count += it->highlight_count;
        }

        auto &e              = roomNotificationCache[it.key()];
        e.highlight_count    = it->highlight_count;
        e.notification_count = it->notification_count;
        globalUnreads.notification_count += it->notification_count;
        globalUnreads.highlight_count += it->highlight_count;

        if (std::find(begin(directMessages_), end(directMessages_), it.key().toStdString()) !=
            end(directMessages_)) {
            dmUnreads.notification_count += it->notification_count;
            dmUnreads.highlight_count += it->highlight_count;
        }
    }

    // NOTE(Nico): We build a forrest from the Directed Cyclic(!) Graph of spaces. To do that we
    // start with orphan spaces at the top. This leaves out some space circles, but there is no good
    // way to break that cycle imo anyway. Then we carefully walk a tree down from each root in our
    // forrest, carefully checking not to run in a circle and get lost forever.
    // TODO(Nico): Optimize this. We can do this with a lot fewer allocations and checks.
    for (const auto &space : isSpace) {
        spaceParents[space];
        for (const auto &p : cache::client()->getParentRoomIds(space)) {
            spaceParents[space].insert(p);
            spaceChilds[p].insert(space);
        }
    }

    temptree spacetree;
    std::vector<std::string> path;
    for (const auto &space : isSpace) {
        if (!spaceParents[space].empty())
            continue;

        spacetree.children[space] = {};
    }
    for (const auto &space : spacetree.children) {
        addChildren(spacetree, path, space.first, spaceChilds);
    }

    // NOTE(Nico): This flattens the tree into a list, preserving the depth at each element.
    spacetree.flatten(spaceOrder_);

    for (const auto &t : ts)
        tags_.push_back(QString::fromStdString(t));

    spaceOrder_.restoreCollapsed();

    for (auto &space : spaceOrder_.tree) {
        for (const auto &c : cache::client()->getChildRoomIds(space.id.toStdString())) {
            const auto &counts = roomNotificationCache[QString::fromStdString(c)];
            space.notificationCounts.highlight_count += counts.highlight_count;
            space.notificationCounts.notification_count += counts.notification_count;
        }
    }

    endResetModel();

    emit tagsChanged();
    emit hiddenTagsChanged();
    emit containsSubspacesChanged();

    setCurrentTagId(UserSettings::instance()->currentTagId());
}

void
CommunitiesModel::FlatTree::storeCollapsed()
{
    QList<QStringList> elements;

    int depth = -1;

    QStringList current;
    elements.reserve(static_cast<int>(tree.size()));

    for (const auto &e : tree) {
        if (e.depth > depth) {
            current.push_back(e.id);
        } else if (e.depth == depth) {
            current.back() = e.id;
        } else {
            current.pop_back();
            current.back() = e.id;
        }

        if (e.collapsed)
            elements.push_back(current);
    }

    UserSettings::instance()->setCollapsedSpaces(elements);
}
void
CommunitiesModel::FlatTree::restoreCollapsed()
{
    QList<QStringList> elements = UserSettings::instance()->collapsedSpaces();

    int depth = -1;

    QStringList current;

    for (auto &e : tree) {
        if (e.depth > depth) {
            current.push_back(e.id);
        } else if (e.depth == depth) {
            current.back() = e.id;
        } else {
            current.pop_back();
            current.back() = e.id;
        }

        if (elements.contains(current))
            e.collapsed = true;
    }
}

void
CommunitiesModel::clear()
{
    beginResetModel();
    tags_.clear();
    endResetModel();
    resetCurrentTagId();

    emit tagsChanged();
}

void
CommunitiesModel::sync(const mtx::responses::Sync &sync_)
{
    bool tagsUpdated  = false;
    const auto userid = http::client()->user_id().to_string();

    for (const auto &[roomid, room] : sync_.rooms.join) {
        for (const auto &e : room.account_data.events)
            if (std::holds_alternative<
                  mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) {
                tagsUpdated = true;
            }
        for (const auto &e : room.state.events) {
            if (std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Child>>(
                  e) ||
                std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Parent>>(
                  e))
                tagsUpdated = true;

            if (auto ev = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(&e);
                ev && ev->state_key == userid)
                tagsUpdated = true;
        }
        for (const auto &e : room.timeline.events) {
            if (std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Child>>(
                  e) ||
                std::holds_alternative<mtx::events::StateEvent<mtx::events::state::space::Parent>>(
                  e))
                tagsUpdated = true;

            if (auto ev = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(&e);
                ev && ev->state_key == userid)
                tagsUpdated = true;
        }

        auto roomId            = QString::fromStdString(roomid);
        auto &oldUnreads       = roomNotificationCache[roomId];
        auto notificationCDiff = -static_cast<int64_t>(oldUnreads.notification_count) +
                                 static_cast<int64_t>(room.unread_notifications.notification_count);
        auto highlightCDiff = -static_cast<int64_t>(oldUnreads.highlight_count) +
                              static_cast<int64_t>(room.unread_notifications.highlight_count);

        auto applyDiff = [notificationCDiff,
                          highlightCDiff](mtx::responses::UnreadNotifications &n) {
            n.highlight_count    = static_cast<int64_t>(n.highlight_count) + highlightCDiff;
            n.notification_count = static_cast<int64_t>(n.notification_count) + notificationCDiff;
        };
        if (highlightCDiff || notificationCDiff) {
            // bool hidden = hiddenTagIds_.contains(roomId);
            applyDiff(globalUnreads);
            emit dataChanged(index(0),
                             index(0),
                             {
                               UnreadMessages,
                               HasLoudNotification,
                             });
            if (std::find(begin(directMessages_), end(directMessages_), roomid) !=
                end(directMessages_)) {
                applyDiff(dmUnreads);
                emit dataChanged(index(1),
                                 index(1),
                                 {
                                   UnreadMessages,
                                   HasLoudNotification,
                                 });
            }

            auto spaces = cache::client()->getParentRoomIds(roomid);
            auto tags   = cache::singleRoomInfo(roomid).tags;

            for (const auto &t : tags) {
                auto tagId = QString::fromStdString(t);
                applyDiff(tagNotificationCache[tagId]);
                int idx = tags_.indexOf(tagId) + 2 + spaceOrder_.size();
                emit dataChanged(index(idx),
                                 index(idx),
                                 {
                                   UnreadMessages,
                                   HasLoudNotification,
                                 });
            }

            for (const auto &s : spaces) {
                auto spaceId = QString::fromStdString(s);

                for (int i = 0; i < spaceOrder_.size(); i++) {
                    if (spaceOrder_.tree[i].id != spaceId)
                        continue;

                    applyDiff(spaceOrder_.tree[i].notificationCounts);

                    int idx = i;
                    do {
                        emit dataChanged(index(idx + 2),
                                         index(idx + 2),
                                         {
                                           UnreadMessages,
                                           HasLoudNotification,
                                         });
                        idx = spaceOrder_.parent(idx);
                    } while (idx != -1);
                }
            }
        }

        roomNotificationCache[roomId] = room.unread_notifications;
    }
    for (const auto &[roomid, room] : sync_.rooms.leave) {
        (void)room;
        if (spaces_.count(QString::fromStdString(roomid)))
            tagsUpdated = true;
        if (hiddenTagIds_.contains(QString::fromStdString("space:" + roomid))) {
            hiddenTagIds_.removeAll(QString::fromStdString("space:" + roomid));
            UserSettings::instance()->setHiddenTags(hiddenTagIds_);
            tagsUpdated = true;
        }
    }
    for (const auto &e : sync_.account_data.events) {
        if (auto event =
              std::get_if<mtx::events::AccountDataEvent<mtx::events::account_data::Direct>>(&e)) {
            directMessages_.clear();
            for (const auto &[userId, roomIds] : event->content.user_to_rooms)
                for (const auto &roomId : roomIds)
                    directMessages_.push_back(roomId);
            tagsUpdated = true;
            break;
        }
    }

    if (tagsUpdated)
        initializeSidebar();
}

void
CommunitiesModel::setCurrentTagId(const QString &tagId)
{
    if (currentTagId_ == tagId)
        return;

    if (tagId.startsWith(QLatin1String("tag:"))) {
        auto tag = tagId.mid(4);
        for (const auto &t : std::as_const(tags_)) {
            if (t == tag) {
                this->currentTagId_ = tagId;
                UserSettings::instance()->setCurrentTagId(tagId);
                emit currentTagIdChanged(currentTagId_);
                return;
            }
        }
    } else if (tagId.startsWith(QLatin1String("space:"))) {
        auto tag = tagId.mid(6);
        for (const auto &t : spaceOrder_.tree) {
            if (t.id == tag) {
                this->currentTagId_ = tagId;
                UserSettings::instance()->setCurrentTagId(tagId);
                emit currentTagIdChanged(currentTagId_);
                return;
            }
        }
    } else if (tagId == QLatin1String("dm")) {
        this->currentTagId_ = tagId;
        UserSettings::instance()->setCurrentTagId(tagId);
        emit currentTagIdChanged(currentTagId_);
        return;
    }

    this->currentTagId_ = QLatin1String("");
    UserSettings::instance()->setCurrentTagId(tagId);
    emit currentTagIdChanged(currentTagId_);
}

bool
CommunitiesModel::trySwitchToSpace(const QString &tag)
{
    for (const auto &t : spaceOrder_.tree) {
        if (t.id == tag) {
            this->currentTagId_ = "space:" + tag;
            UserSettings::instance()->setCurrentTagId(tag);
            emit currentTagIdChanged(currentTagId_);
            return true;
        }
    }

    return false;
}

void
CommunitiesModel::toggleTagId(QString tagId)
{
    if (hiddenTagIds_.contains(tagId))
        hiddenTagIds_.removeOne(tagId);
    else
        hiddenTagIds_.push_back(tagId);

    // sanity check to remove stale spaces
    hiddenTagIds_.removeIf([this](const QString &value) {
        return value.startsWith("space:") && !spaces_.contains(value.mid(6));
    });

    UserSettings::instance()->setHiddenTags(hiddenTagIds_);

    if (tagId.startsWith(QLatin1String("tag:"))) {
        auto idx = tags_.indexOf(tagId.mid(4));
        if (idx != -1)
            emit dataChanged(
              index(idx + 1 + spaceOrder_.size()), index(idx + 1 + spaceOrder_.size()), {Hidden});
    } else if (tagId.startsWith(QLatin1String("space:"))) {
        auto idx = spaceOrder_.indexOf(tagId.mid(6));
        if (idx != -1)
            emit dataChanged(index(idx + 1), index(idx + 1), {Hidden});
    } else if (tagId == QLatin1String("dm")) {
        emit dataChanged(index(1), index(1), {Hidden});
    }

    emit hiddenTagsChanged();
}

void
CommunitiesModel::toggleTagMute(QString tagId)
{
    if (tagId.isEmpty())
        tagId = QStringLiteral("global");

    if (mutedTagIds_.contains(tagId))
        mutedTagIds_.removeOne(tagId);
    else
        mutedTagIds_.push_back(tagId);
    UserSettings::instance()->setMutedTags(mutedTagIds_);

    if (tagId.startsWith(QLatin1String("tag:"))) {
        auto idx = tags_.indexOf(tagId.mid(4));
        if (idx != -1)
            emit dataChanged(index(idx + 2 + spaceOrder_.size()),
                             index(idx + 2 + spaceOrder_.size()));
    } else if (tagId.startsWith(QLatin1String("space:"))) {
        auto idx = spaceOrder_.indexOf(tagId.mid(6));
        if (idx != -1)
            emit dataChanged(index(idx + 2), index(idx + 2));
    } else if (tagId == QLatin1String("dm")) {
        emit dataChanged(index(1), index(1));
    } else if (tagId == QLatin1String("global")) {
        emit dataChanged(index(0), index(0));
    }
}

FilteredCommunitiesModel::FilteredCommunitiesModel(CommunitiesModel *model, QObject *parent)
  : QSortFilterProxyModel(parent)
{
    setSourceModel(model);
    setDynamicSortFilter(true);
    sort(0);
}

namespace {
enum Categories
{
    World,
    Direct,
    Favourites,
    Server,
    LowPrio,
    Space,
    UserTag,
};

Categories
tagIdToCat(const QString &tagId)
{
    if (tagId.isEmpty())
        return World;
    else if (tagId == QLatin1String("dm"))
        return Direct;
    else if (tagId == QLatin1String("tag:m.favourite"))
        return Favourites;
    else if (tagId == QLatin1String("tag:m.server_notice"))
        return Server;
    else if (tagId == QLatin1String("tag:m.lowpriority"))
        return LowPrio;
    else if (tagId.startsWith(QLatin1String("space:")))
        return Space;
    else
        return UserTag;
}
}

bool
FilteredCommunitiesModel::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());

    Categories leftCat = tagIdToCat(sourceModel()->data(left_idx, CommunitiesModel::Id).toString());
    Categories rightCat =
      tagIdToCat(sourceModel()->data(right_idx, CommunitiesModel::Id).toString());

    if (leftCat != rightCat)
        return leftCat < rightCat;

    if (leftCat == Space) {
        return left.row() < right.row();
    }

    QString leftName  = sourceModel()->data(left_idx, CommunitiesModel::DisplayName).toString();
    QString rightName = sourceModel()->data(right_idx, CommunitiesModel::DisplayName).toString();

    return leftName.compare(rightName, Qt::CaseInsensitive) < 0;
}
bool
FilteredCommunitiesModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
{
    CommunitiesModel *m = qobject_cast<CommunitiesModel *>(this->sourceModel());
    if (!m)
        return true;

    if (sourceRow < 2 || sourceRow - 2 >= m->spaceOrder_.size())
        return true;

    auto idx = sourceRow - 2;

    while (idx >= 0 && m->spaceOrder_.tree[idx].depth > 0) {
        idx = m->spaceOrder_.parent(idx);

        if (idx >= 0 && m->spaceOrder_.tree.at(idx).collapsed)
            return false;
    }

    return true;
}

QVariantList
CommunitiesModel::spaceChildrenListFromIndex(const QString &room, int idx) const
{
    if (idx < -1)
        return {};

    auto room_ = room.toStdString();

    int begin = idx + 1;
    int end   = idx >= 0 ? this->spaceOrder_.lastChild(idx) + 1 : this->spaceOrder_.size();
    QVariantList ret;

    bool canSendParent = Permissions(room).canChange(qml_mtx_events::SpaceParent);

    for (int i = begin; i < end; i++) {
        const auto &e = spaceOrder_.tree[i];
        if (e.depth == spaceOrder_.tree[begin].depth && spaces_.count(e.id)) {
            bool canSendChild = Permissions(e.id).canChange(qml_mtx_events::SpaceChild);
            // For now hide the space, if we can't send any child, since then the only allowed
            // action would be removing a space and even that only works if it currently only has a
            // parent set in the child.
            if (!canSendChild)
                continue;

            auto spaceId = e.id.toStdString();
            auto child =
              cache::client()->getStateEvent<mtx::events::state::space::Child>(spaceId, room_);
            auto parent =
              cache::client()->getStateEvent<mtx::events::state::space::Parent>(room_, spaceId);

            bool childValid =
              child && !child->content.via.value_or(std::vector<std::string>{}).empty();
            bool parentValid =
              parent && !parent->content.via.value_or(std::vector<std::string>{}).empty();
            bool canonical = parent && parent->content.canonical;

            if (e.id == room) {
                canonical = parentValid = childValid = canSendChild = canSendParent = false;
            }

            ret.push_back(
              QVariant::fromValue(SpaceItem(e.id,
                                            QString::fromStdString(spaces_.at(e.id).name),
                                            i,
                                            childValid,
                                            parentValid,
                                            canonical,
                                            canSendChild,
                                            canSendParent)));
        }
    }

    // nhlog::ui()->critical("Returning {} spaces", ret.size());
    return ret;
}

void
CommunitiesModel::updateSpaceStatus(QString space,
                                    QString room,
                                    bool setParent,
                                    bool setChild,
                                    bool canonical) const
{
    nhlog::ui()->critical("Setting space {} children {}: {} {} {}",
                          space.toStdString(),
                          room.toStdString(),
                          setParent,
                          setChild,
                          canonical);
    auto child =
      cache::client()
        ->getStateEvent<mtx::events::state::space::Child>(space.toStdString(), room.toStdString())
        .value_or(mtx::events::StateEvent<mtx::events::state::space::Child>{})
        .content;
    auto parent =
      cache::client()
        ->getStateEvent<mtx::events::state::space::Parent>(room.toStdString(), space.toStdString())
        .value_or(mtx::events::StateEvent<mtx::events::state::space::Parent>{})
        .content;

    if (setChild) {
        if (!child.via || child.via->empty()) {
            child.via       = utils::roomVias(room.toStdString());
            child.suggested = true;

            http::client()->send_state_event(
              space.toStdString(),
              room.toStdString(),
              child,
              [space, room](mtx::responses::EventId, mtx::http::RequestErr err) {
                  if (err) {
                      ChatPage::instance()->showNotification(
                        tr("Failed to update community: %1")
                          .arg(QString::fromStdString(err->matrix_error.error)));
                      nhlog::net()->error("Failed to update child {} of {}: {}",
                                          room.toStdString(),
                                          space.toStdString(),
                                          *err);
                  }
              });
        }
    } else {
        if (child.via && !child.via->empty()) {
            http::client()->send_state_event(
              space.toStdString(),
              room.toStdString(),
              mtx::events::state::space::Child{},
              [space, room](mtx::responses::EventId, mtx::http::RequestErr err) {
                  if (err) {
                      ChatPage::instance()->showNotification(
                        tr("Failed to delete room from community: %1")
                          .arg(QString::fromStdString(err->matrix_error.error)));
                      nhlog::net()->error("Failed to delete child {} of {}: {}",
                                          room.toStdString(),
                                          space.toStdString(),
                                          *err);
                  }
              });
        }
    }

    if (setParent) {
        if (!parent.via || parent.via->empty() || canonical != parent.canonical) {
            parent.via       = utils::roomVias(room.toStdString());
            parent.canonical = canonical;

            http::client()->send_state_event(
              room.toStdString(),
              space.toStdString(),
              parent,
              [space, room](mtx::responses::EventId, mtx::http::RequestErr err) {
                  if (err) {
                      ChatPage::instance()->showNotification(
                        tr("Failed to update community for room: %1")
                          .arg(QString::fromStdString(err->matrix_error.error)));
                      nhlog::net()->error("Failed to update parent {} of {}: {}",
                                          space.toStdString(),
                                          room.toStdString(),
                                          *err);
                  }
              });
        }
    } else {
        if (parent.via && !parent.via->empty()) {
            http::client()->send_state_event(
              room.toStdString(),
              space.toStdString(),
              mtx::events::state::space::Parent{},
              [space, room](mtx::responses::EventId, mtx::http::RequestErr err) {
                  if (err) {
                      ChatPage::instance()->showNotification(
                        tr("Failed to remove community from room: %1")
                          .arg(QString::fromStdString(err->matrix_error.error)));
                      nhlog::net()->error("Failed to delete parent {} of {}: {}",
                                          space.toStdString(),
                                          room.toStdString(),
                                          *err);
                  }
              });
        }
    }
}