// SPDX-FileCopyrightText: 2010 Jeremy Lainé <jeremy.laine@m4x.org>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppMucManager.h"

#include "QXmppClient.h"
#include "QXmppConstants_p.h"
#include "QXmppDiscoveryManager.h"
#include "QXmppMessage.h"
#include "QXmppMucIq.h"
#include "QXmppTask.h"
#include "QXmppUtils.h"

#include "StringLiterals.h"

#include <QDomElement>
#include <QMap>

using namespace QXmpp;
using namespace QXmpp::Private;

class QXmppMucManagerPrivate
{
public:
    QMap<QString, QXmppMucRoom *> rooms;
};

class QXmppMucRoomPrivate
{
public:
    QString ownJid() const { return jid + u'/' + nickName; }
    QXmppClient *client;
    QXmppDiscoveryManager *discoManager;
    QXmppMucRoom::Actions allowedActions;
    QString jid;
    QString name;
    QMap<QString, QXmppPresence> participants;
    QString password;
    QMap<QString, QXmppMucItem> permissions;
    QSet<QString> permissionsQueue;
    QString nickName;
    QString subject;
};

/*! Constructs a new QXmppMucManager. */
QXmppMucManager::QXmppMucManager()
    : d(std::make_unique<QXmppMucManagerPrivate>())
{
}

QXmppMucManager::~QXmppMucManager() = default;

/*!
    Adds the chat room with bare JID \a roomJid to the set of managed rooms.
*/

QXmppMucRoom *QXmppMucManager::addRoom(const QString &roomJid)
{
    QXmppMucRoom *room = d->rooms.value(roomJid);
    if (!room) {
        room = new QXmppMucRoom(client(), roomJid, this);
        d->rooms.insert(roomJid, room);
        connect(room, &QObject::destroyed,
                this, &QXmppMucManager::_q_roomDestroyed);

        // emit signal
        Q_EMIT roomAdded(room);
    }
    return room;
}

/*! Returns the list of managed rooms. */
QList<QXmppMucRoom *> QXmppMucManager::rooms() const
{
    return d->rooms.values();
}

QStringList QXmppMucManager::discoveryFeatures() const
{
    // XEP-0045: Multi-User Chat
    return {
        staticString(ns_muc),
        staticString(ns_muc_admin),
        staticString(ns_muc_owner),
        staticString(ns_muc_user),
        staticString(ns_conference),
    };
}

bool QXmppMucManager::handleStanza(const QDomElement &element)
{
    if (element.tagName() == u"iq") {
        if (QXmppMucAdminIq::isMucAdminIq(element)) {
            QXmppMucAdminIq iq;
            iq.parse(element);

            QXmppMucRoom *room = d->rooms.value(iq.from());
            if (room && iq.type() == QXmppIq::Result && room->d->permissionsQueue.remove(iq.id())) {
                const auto items = iq.items();
                for (const auto &item : items) {
                    const QString jid = item.jid();
                    if (!room->d->permissions.contains(jid)) {
                        room->d->permissions.insert(jid, item);
                    }
                }
                if (room->d->permissionsQueue.isEmpty()) {
                    Q_EMIT room->permissionsReceived(room->d->permissions.values());
                }
                return true;
            }
        } else if (QXmppMucOwnerIq::isMucOwnerIq(element)) {
            QXmppMucOwnerIq iq;
            iq.parse(element);

            QXmppMucRoom *room = d->rooms.value(iq.from());
            if (room && iq.type() == QXmppIq::Result && !iq.form().isNull()) {
                Q_EMIT room->configurationReceived(iq.form());
                return true;
            }
        }
    }
    return false;
}

void QXmppMucManager::onRegistered(QXmppClient *client)
{
    connect(client, &QXmppClient::messageReceived,
            this, &QXmppMucManager::_q_messageReceived);
}

void QXmppMucManager::onUnregistered(QXmppClient *client)
{
    disconnect(client, &QXmppClient::messageReceived,
               this, &QXmppMucManager::_q_messageReceived);
}

void QXmppMucManager::_q_messageReceived(const QXmppMessage &msg)
{
    if (msg.type() != QXmppMessage::Normal) {
        return;
    }

    // process room invitations
    if (const auto inv = msg.mucDirectInvitation(); inv && !inv->jid().isEmpty()) {
        const auto &roomJid = inv->jid();
        if (!d->rooms.contains(roomJid) || !d->rooms.value(roomJid)->isJoined()) {
            Q_EMIT invitationReceived(roomJid, msg.from(), inv->reason());
        }
    }
}

void QXmppMucManager::_q_roomDestroyed(QObject *object)
{
    const QString key = d->rooms.key(static_cast<QXmppMucRoom *>(object));
    d->rooms.remove(key);
}

/*!
    Constructs a new QXmppMucRoom with the given \a parent.
*/

QXmppMucRoom::QXmppMucRoom(QXmppClient *client, const QString &jid, QObject *parent)
    : QObject(parent),
      d(std::make_unique<QXmppMucRoomPrivate>())
{
    d->allowedActions = NoAction;
    d->client = client;
    d->discoManager = client->findExtension<QXmppDiscoveryManager>();
    d->jid = jid;

    connect(d->client, &QXmppClient::disconnected,
            this, &QXmppMucRoom::_q_disconnected);

    connect(d->client, &QXmppClient::messageReceived,
            this, &QXmppMucRoom::_q_messageReceived);

    connect(d->client, &QXmppClient::presenceReceived,
            this, &QXmppMucRoom::_q_presenceReceived);

    // convenience signals for properties
    connect(this, &QXmppMucRoom::joined, this, &QXmppMucRoom::isJoinedChanged);
    connect(this, &QXmppMucRoom::left, this, &QXmppMucRoom::isJoinedChanged);
}

QXmppMucRoom::~QXmppMucRoom() = default;

/*! Returns the actions you are allowed to perform on the room. */
QXmppMucRoom::Actions QXmppMucRoom::allowedActions() const
{
    return d->allowedActions;
}

/*!
    Bans the specified user from the chat room.

    The specified \a jid is the Bare JID of the form "user@host". \a reason gives the reason
    for the ban.

    Returns true if the request was sent, false otherwise.
*/
bool QXmppMucRoom::ban(const QString &jid, const QString &reason)
{
    if (!QXmppUtils::jidToResource(jid).isEmpty()) {
        qWarning("QXmppMucRoom::ban expects a bare JID");
        return false;
    }

    QXmppMucItem item;
    item.setAffiliation(QXmppMucItem::OutcastAffiliation);
    item.setJid(jid);
    item.setReason(reason);

    QXmppMucAdminIq iq;
    iq.setType(QXmppIq::Set);
    iq.setTo(d->jid);
    iq.setItems(QList<QXmppMucItem>() << item);

    return d->client->sendLegacy(iq);
}

/*! Returns true if you are currently in the room. */
bool QXmppMucRoom::isJoined() const
{
    return d->participants.contains(d->ownJid());
}

/*! Returns the chat room's bare JID. */
QString QXmppMucRoom::jid() const
{
    return d->jid;
}

/*!
    Joins the chat room.

    Returns true if the request was sent, false otherwise.
*/
bool QXmppMucRoom::join()
{
    if (isJoined() || d->nickName.isEmpty()) {
        return false;
    }

    // reflect our current presence in the chat room
    QXmppPresence packet = d->client->clientPresence();
    packet.setTo(d->ownJid());
    packet.setType(QXmppPresence::Available);
    packet.setMucPassword(d->password);
    packet.setMucSupported(true);
    return d->client->sendLegacy(packet);
}

/*!
    Kicks the specified user from the chat room.

    The specified \a jid is the Occupant JID of the form "room@service/nick". \a reason gives
    the reason for the kick.

    Returns true if the request was sent, false otherwise.
*/
bool QXmppMucRoom::kick(const QString &jid, const QString &reason)
{
    QXmppMucItem item;
    item.setNick(QXmppUtils::jidToResource(jid));
    item.setRole(QXmppMucItem::NoRole);
    item.setReason(reason);

    QXmppMucAdminIq iq;
    iq.setType(QXmppIq::Set);
    iq.setTo(d->jid);
    iq.setItems(QList<QXmppMucItem>() << item);

    return d->client->sendLegacy(iq);
}

/*!
    Leaves the chat room with an optional \a message.

    Returns true if the request was sent, false otherwise.
*/
bool QXmppMucRoom::leave(const QString &message)
{
    QXmppPresence packet;
    packet.setTo(d->ownJid());
    packet.setType(QXmppPresence::Unavailable);
    packet.setStatusText(message);
    return d->client->sendLegacy(packet);
}

/*!
    Returns the chat room's human-readable name.

    This name will only be available after the room has been joined.
*/
QString QXmppMucRoom::name() const
{
    return d->name;
}

/*! Returns your own nickname. */
QString QXmppMucRoom::nickName() const
{
    return d->nickName;
}

/*!
    Invites the user with JID \a jid to the chat room with the given \a reason.

    Returns true if the request was sent, false otherwise.
*/
bool QXmppMucRoom::sendInvitation(const QString &jid, const QString &reason)
{
    QXmppMessage message;
    message.setTo(jid);
    message.setType(QXmppMessage::Normal);
    message.setMucDirectInvitation(QXmpp::Muc::DirectInvitation(d->jid, {}, reason));
    return d->client->sendLegacy(message);
}

/*!
    Sends a message with body \a text to the room.

    This is just a helper function, you can as well also send a message to the
    channel manually by setting the message type to 'groupchat' and addressing
    the JID of the MUC room.

    Returns true if the request was sent, false otherwise.
*/
bool QXmppMucRoom::sendMessage(const QString &text)
{
    QXmppMessage msg;
    msg.setTo(d->jid);
    msg.setType(QXmppMessage::GroupChat);
    msg.setBody(text);
    return d->client->sendLegacy(msg);
}

/*!
    Sets your own nickname to \a nickName.

    You need to set your nickname before calling join().
*/
void QXmppMucRoom::setNickName(const QString &nickName)
{
    if (nickName == d->nickName) {
        return;
    }

    // if we had already joined the room, request nickname change
    if (isJoined()) {
        QXmppPresence packet = d->client->clientPresence();
        packet.setTo(d->jid + u'/' + nickName);
        packet.setType(QXmppPresence::Available);
        d->client->send(std::move(packet));
    } else {
        d->nickName = nickName;
        Q_EMIT nickNameChanged(nickName);
    }
}

/*!
    Returns the "Full JID" of the given participant.

    The specified \a jid is the Occupant JID of the form "room@service/nick".
*/
QString QXmppMucRoom::participantFullJid(const QString &jid) const
{
    if (d->participants.contains(jid)) {
        return d->participants.value(jid).mucParticipantItem().jid();
    } else {
        return QString();
    }
}

/*!
    Returns the presence for the given participant.

    The specified \a jid is the Occupant JID of the form "room@service/nick".
*/
QXmppPresence QXmppMucRoom::participantPresence(const QString &jid) const
{
    if (d->participants.contains(jid)) {
        return d->participants.value(jid);
    }

    QXmppPresence presence;
    presence.setFrom(jid);
    presence.setType(QXmppPresence::Unavailable);
    return presence;
}

/*!
    Returns the list of participant JIDs.

    These JIDs are Occupant JIDs of the form "room@service/nick".
*/
QStringList QXmppMucRoom::participants() const
{
    return d->participants.keys();
}

/*! Returns the chat room password. */
QString QXmppMucRoom::password() const
{
    return d->password;
}

/*!
    Sets the chat room \a password.
*/
void QXmppMucRoom::setPassword(const QString &password)
{
    d->password = password;
}

/*! Returns the room's subject. */
QString QXmppMucRoom::subject() const
{
    return d->subject;
}

/*!
    Sets the chat room's \a subject.
*/
void QXmppMucRoom::setSubject(const QString &subject)
{
    QXmppMessage msg;
    msg.setTo(d->jid);
    msg.setType(QXmppMessage::GroupChat);
    msg.setSubject(subject);
    d->client->send(std::move(msg));
}

/*!
    Request the configuration form for the chat room.

    Returns true if the request was sent, false otherwise.

    \sa configurationReceived()
*/
bool QXmppMucRoom::requestConfiguration()
{
    QXmppMucOwnerIq iq;
    iq.setTo(d->jid);
    return d->client->sendLegacy(iq);
}

/*!
    Send the configuration \a form for the chat room.

    Returns true if the request was sent, false otherwise.
*/
bool QXmppMucRoom::setConfiguration(const QXmppDataForm &form)
{
    QXmppMucOwnerIq iqPacket;
    iqPacket.setType(QXmppIq::Set);
    iqPacket.setTo(d->jid);
    iqPacket.setForm(form);
    return d->client->sendLegacy(iqPacket);
}

/*!
    Request the room's permissions.

    Returns true if the request was sent, false otherwise.

    \sa permissionsReceived()
*/
bool QXmppMucRoom::requestPermissions()
{
    QList<QXmppMucItem::Affiliation> affiliations;
    affiliations << QXmppMucItem::OwnerAffiliation;
    affiliations << QXmppMucItem::AdminAffiliation;
    affiliations << QXmppMucItem::MemberAffiliation;
    affiliations << QXmppMucItem::OutcastAffiliation;

    d->permissions.clear();
    d->permissionsQueue.clear();
    for (const auto &affiliation : std::as_const(affiliations)) {
        QXmppMucItem item;
        item.setAffiliation(affiliation);

        QXmppMucAdminIq iq;
        iq.setTo(d->jid);
        iq.setItems(QList<QXmppMucItem>() << item);
        if (!d->client->sendLegacy(iq)) {
            return false;
        }
        d->permissionsQueue += iq.id();
    }
    return true;
}

/*!
    Sets the room's \a permissions.

    Returns true if the request was sent, false otherwise.
*/
bool QXmppMucRoom::setPermissions(const QList<QXmppMucItem> &permissions)
{
    QList<QXmppMucItem> items;

    // Process changed members
    for (const auto &item : std::as_const(permissions)) {
        const QString jid = item.jid();
        if (d->permissions.value(jid).affiliation() != item.affiliation()) {
            items << item;
        }
        d->permissions.remove(jid);
    }

    // Process deleted members
    const auto &jids = d->permissions.keys();
    for (const auto &jid : jids) {
        QXmppMucItem item;
        item.setAffiliation(QXmppMucItem::NoAffiliation);
        item.setJid(jid);
        items << item;
        d->permissions.remove(jid);
    }

    // Don't send request if there are no changes
    if (items.isEmpty()) {
        return false;
    }

    QXmppMucAdminIq iq;
    iq.setTo(d->jid);
    iq.setType(QXmppIq::Set);
    iq.setItems(items);
    return d->client->sendLegacy(iq);
}

void QXmppMucRoom::_q_disconnected()
{
    const bool wasJoined = isJoined();

    // clear chat room participants
    const QStringList removed = d->participants.keys();
    d->participants.clear();
    for (const auto &jid : removed) {
        Q_EMIT participantRemoved(jid);
    }
    Q_EMIT participantsChanged();

    // update available actions
    if (d->allowedActions != NoAction) {
        d->allowedActions = NoAction;
        Q_EMIT allowedActionsChanged(d->allowedActions);
    }

    // emit "left" signal if we had joined the room
    if (wasJoined) {
        Q_EMIT left();
    }
}

void QXmppMucRoom::_q_messageReceived(const QXmppMessage &message)
{
    if (QXmppUtils::jidToBareJid(message.from()) != d->jid) {
        return;
    }

    // handle message subject
    const QString subject = message.subject();
    if (!subject.isEmpty()) {
        d->subject = subject;
        Q_EMIT subjectChanged(subject);
    }

    Q_EMIT messageReceived(message);
}

void QXmppMucRoom::_q_presenceReceived(const QXmppPresence &presence)
{
    const QString jid = presence.from();

    // if our own presence changes, reflect it in the chat room
    if (isJoined() && jid == d->client->configuration().jid()) {
        QXmppPresence packet = d->client->clientPresence();
        packet.setTo(d->ownJid());
        d->client->send(std::move(packet));
    }

    if (QXmppUtils::jidToBareJid(jid) != d->jid) {
        return;
    }

    if (presence.type() == QXmppPresence::Available) {
        const bool added = !d->participants.contains(jid);
        d->participants.insert(jid, presence);

        // refresh allowed actions
        if (jid == d->ownJid()) {

            auto mucItem = presence.mucParticipantItem();
            Actions newActions = NoAction;

            // role
            if (mucItem.role() == Muc::Role::Moderator) {
                newActions |= (KickAction | SubjectAction);
            }

            // affiliation
            if (mucItem.affiliation() == Muc::Affiliation::Owner) {
                newActions |= (ConfigurationAction | PermissionsAction | SubjectAction);
            } else if (mucItem.affiliation() == Muc::Affiliation::Admin) {
                newActions |= (PermissionsAction | SubjectAction);
            }

            if (newActions != d->allowedActions) {
                d->allowedActions = newActions;
                Q_EMIT allowedActionsChanged(d->allowedActions);
            }
        }

        if (added) {
            Q_EMIT participantAdded(jid);
            Q_EMIT participantsChanged();
            if (jid == d->ownJid()) {
                // request room information
                if (d->discoManager) {
                    d->discoManager->info(d->jid).then(this, [this](auto &&result) {
                        if (!hasValue(result)) {
                            return;
                        }

                        QString name;
                        const auto &identities = getValue(result).identities();
                        for (const auto &identity : identities) {
                            if (identity.category() == u"conference") {
                                name = identity.name();
                                break;
                            }
                        }

                        if (name != d->name) {
                            d->name = name;
                            Q_EMIT nameChanged(name);
                        }
                    });
                }

                Q_EMIT joined();
            }
        } else {
            Q_EMIT participantChanged(jid);
        }
    } else if (presence.type() == QXmppPresence::Unavailable) {
        if (d->participants.contains(jid)) {
            d->participants.insert(jid, presence);

            Q_EMIT participantRemoved(jid);
            d->participants.remove(jid);
            Q_EMIT participantsChanged();

            // check whether this was our own presence
            if (jid == d->ownJid()) {
                const QString newNick = presence.mucParticipantItem().nick();
                if (!newNick.isEmpty() && newNick != d->nickName) {
                    d->nickName = newNick;
                    Q_EMIT nickNameChanged(newNick);
                    return;
                }

                // check whether we were kicked
                if (presence.mucStatusCodes().contains(307)) {
                    const QString actor = presence.mucParticipantItem().actor();
                    const QString reason = presence.mucParticipantItem().reason();
                    Q_EMIT kicked(actor, reason);
                }

                // clear chat room participants
                const QStringList removed = d->participants.keys();
                d->participants.clear();
                for (const auto &jid : removed) {
                    Q_EMIT participantRemoved(jid);
                }
                Q_EMIT participantsChanged();

                // update available actions
                if (d->allowedActions != NoAction) {
                    d->allowedActions = NoAction;
                    Q_EMIT allowedActionsChanged(d->allowedActions);
                }

                // notify user we left the room
                Q_EMIT left();
            }
        }
    } else if (presence.type() == QXmppPresence::Error) {
        if (presence.isMucSupported()) {
            // emit error
            Q_EMIT error(presence.error());

            // notify the user we left the room
            Q_EMIT left();
        }
    }
}
