// SPDX-FileCopyrightText: 2019-2020 Black Hat <bhat@encom.eu.org>
// SPDX-License-Identifier: GPL-3.0-only

#include "publicroomlistmodel.h"

#include "neochatconnection.h"
#include "publicroomlist_logging.h"

using namespace Quotient;

class NeoChatQueryPublicRoomsJob : public QueryPublicRoomsJob
{
public:
    explicit NeoChatQueryPublicRoomsJob(const QString &server = {},
                                        std::optional<int> limit = std::nullopt,
                                        const QString &since = {},
                                        const std::optional<Filter> &filter = std::nullopt,
                                        std::optional<bool> includeAllNetworks = std::nullopt,
                                        const QString &thirdPartyInstanceId = {})
        : QueryPublicRoomsJob(server, limit, since, filter, includeAllNetworks, thirdPartyInstanceId)
    {
        // TODO Remove once we can use libQuotient's job directly
        // This is to make libQuotient happy about results not having the "chunk" field
        setExpectedKeys({});
    }
};

PublicRoomListModel::PublicRoomListModel(QObject *parent)
    : QAbstractListModel(parent)
{
}

NeoChatConnection *PublicRoomListModel::connection() const
{
    return m_connection;
}

void PublicRoomListModel::setConnection(NeoChatConnection *connection)
{
    if (m_connection == connection) {
        return;
    }

    beginResetModel();

    nextBatch = QString();
    attempted = false;
    rooms.clear();
    m_server.clear();

    if (m_connection) {
        m_connection->disconnect(this);
    }

    endResetModel();

    m_connection = connection;

    if (job) {
        job->abandon();
        job = nullptr;
        Q_EMIT searchingChanged();
    }

    if (m_connection) {
        next();
    }

    Q_EMIT connectionChanged();
    Q_EMIT serverChanged();
}

QString PublicRoomListModel::server() const
{
    return m_server;
}

void PublicRoomListModel::setServer(const QString &value)
{
    if (m_server == value) {
        return;
    }

    m_server = value;

    beginResetModel();

    nextBatch = QString();
    attempted = false;
    rooms.clear();

    endResetModel();

    if (job) {
        job->abandon();
        job = nullptr;
        Q_EMIT searchingChanged();
    }

    if (m_connection) {
        next();
    }

    Q_EMIT serverChanged();
}

QString PublicRoomListModel::searchText() const
{
    return m_searchText;
}

void PublicRoomListModel::setSearchText(const QString &value)
{
    if (m_searchText == value) {
        return;
    }

    m_searchText = value;
    Q_EMIT searchTextChanged();

    nextBatch = QString();
    attempted = false;

    if (job) {
        job->abandon();
        job = nullptr;
        Q_EMIT searchingChanged();
    }
}

bool PublicRoomListModel::showOnlySpaces() const
{
    return m_showOnlySpaces;
}

void PublicRoomListModel::setShowOnlySpaces(bool showOnlySpaces)
{
    if (showOnlySpaces == m_showOnlySpaces) {
        return;
    }
    m_showOnlySpaces = showOnlySpaces;
    Q_EMIT showOnlySpacesChanged();

    nextBatch = QString();
    attempted = false;

    if (job) {
        job->abandon();
        job = nullptr;
        Q_EMIT searchingChanged();
    }
}

void PublicRoomListModel::search(int limit)
{
    if (limit < 1 || attempted) {
        return;
    }

    if (job) {
        qCDebug(PublicRoomList) << "Other job running, ignore";
        return;
    }

    next(limit);
}

void PublicRoomListModel::next(int limit)
{
    if (m_connection == nullptr || limit < 1) {
        return;
    }
    m_redirectedText.clear();
    Q_EMIT redirectedChanged();
    m_errorText.clear();
    Q_EMIT errorTextChanged();

    if (job) {
        qCDebug(PublicRoomList) << "Other job running, ignore";
        return;
    }

    QStringList roomTypes;
    if (m_showOnlySpaces) {
        roomTypes += u"m.space"_s;
    }
    job = m_connection->callApi<NeoChatQueryPublicRoomsJob>(m_server, limit, nextBatch, QueryPublicRoomsJob::Filter{m_searchText, roomTypes});
    Q_EMIT searchingChanged();

    connect(job, &BaseJob::finished, this, [this] {
        if (!attempted) {
            beginResetModel();
            rooms.clear();
            endResetModel();

            attempted = true;
        }

        if (job->status() == BaseJob::Success) {
            nextBatch = job->nextBatch();

            if (!job->chunk().isEmpty()) {
                this->beginInsertRows({}, rooms.count(), rooms.count() + job->chunk().count() - 1);
                rooms.append(job->chunk());
                this->endInsertRows();
            }
        } else if (job->error() == BaseJob::ContentAccessError && !m_searchText.isEmpty()) {
            m_redirectedText = job->jsonData()[u"error"_s].toString();
            Q_EMIT redirectedChanged();
        } else {
            m_errorText = job->jsonData()[u"error"_s].toString();
            Q_EMIT errorTextChanged();
        }

        this->job = nullptr;
        Q_EMIT searchingChanged();
    });
}

QVariant PublicRoomListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) {
        return QVariant();
    }

    if (index.row() >= rooms.count()) {
        qCDebug(PublicRoomList) << "something's wrong: index.row() >= rooms.count()";
        return {};
    }
    auto room = rooms.at(index.row());
    if (role == DisplayNameRole) {
        auto displayName = room.name;
        if (!displayName.isEmpty()) {
            return displayName;
        }

        displayName = room.canonicalAlias;
        if (!displayName.isEmpty()) {
            return displayName;
        }

        if (!displayName.isEmpty()) {
            return displayName;
        }

        return room.roomId;
    }
    if (role == AvatarUrlRole) {
        auto avatarUrl = room.avatarUrl;
        if (avatarUrl.isEmpty() || !m_connection) {
            return QUrl();
        }
        return m_connection->makeMediaUrl(avatarUrl);
    }
    if (role == TopicRole) {
        return room.topic;
    }
    if (role == RoomIdRole) {
        return room.roomId;
    }
    if (role == AliasRole) {
        if (!room.canonicalAlias.isEmpty()) {
            return room.canonicalAlias;
        }
        return {};
    }
    if (role == MemberCountRole) {
        return room.numJoinedMembers;
    }
    if (role == AllowGuestsRole) {
        return room.guestCanJoin;
    }
    if (role == WorldReadableRole) {
        return room.worldReadable;
    }
    if (role == IsJoinedRole) {
        if (!m_connection) {
            return {};
        }

        return m_connection->room(room.roomId, JoinState::Join) != nullptr;
    }
    if (role == IsSpaceRole) {
        return room.roomType == u"m.space"_s;
    }

    return {};
}

QHash<int, QByteArray> PublicRoomListModel::roleNames() const
{
    QHash<int, QByteArray> roles;

    roles[DisplayNameRole] = "displayName";
    roles[AvatarUrlRole] = "avatarUrl";
    roles[TopicRole] = "topic";
    roles[RoomIdRole] = "roomId";
    roles[MemberCountRole] = "memberCount";
    roles[AllowGuestsRole] = "allowGuests";
    roles[WorldReadableRole] = "worldReadable";
    roles[IsJoinedRole] = "isJoined";
    roles[IsSpaceRole] = "isSpace";
    roles[AliasRole] = "alias";

    return roles;
}

int PublicRoomListModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid()) {
        return 0;
    }

    return rooms.count();
}

bool PublicRoomListModel::canFetchMore(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    return !nextBatch.isEmpty();
}

void PublicRoomListModel::fetchMore(const QModelIndex &parent)
{
    Q_UNUSED(parent)
    next();
}

bool PublicRoomListModel::searching() const
{
    return job != nullptr;
}

QString PublicRoomListModel::redirectedText() const
{
    return m_redirectedText;
}

QString PublicRoomListModel::errorText() const
{
    return m_errorText;
}

#include "moc_publicroomlistmodel.cpp"
