// SPDX-FileCopyrightText: 2009 Manjeet Dahiya <manjeetdahiya@gmail.com>
// SPDX-FileCopyrightText: 2010 Jeremy Lainé <jeremy.laine@m4x.org>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppRosterIq.h"

#include "QXmppConstants_p.h"
#include "QXmppUtils_p.h"

#include "StringLiterals.h"
#include "XmlWriter.h"

#include <QDomElement>
#include <QSharedData>
#include <QXmlStreamWriter>

using namespace QXmpp::Private;

class QXmppRosterIqPrivate : public QSharedData
{
public:
    QList<QXmppRosterIq::Item> items;
    // RFC 6121 §2.6 Roster Versioning: nullopt means no `ver` attribute; an empty string means
    // `ver=""` (how clients advertise versioning support to the server).
    std::optional<QString> version;
    // Whether the <query> element was present (vs. an <iq type='result'/> with no payload,
    // which servers use to indicate the cached roster is still up-to-date).
    bool hasQuery = false;
    // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements
    bool mixAnnotate = false;
};

QXmppRosterIq::QXmppRosterIq()
    : d(new QXmppRosterIqPrivate)
{
}

/*! Default copy-constructor */
QXmppRosterIq::QXmppRosterIq(const QXmppRosterIq &) = default;
/*! Default move-constructor */
QXmppRosterIq::QXmppRosterIq(QXmppRosterIq &&) = default;
QXmppRosterIq::~QXmppRosterIq() = default;
/*! Default assignment operator */
QXmppRosterIq &QXmppRosterIq::operator=(const QXmppRosterIq &) = default;
/*! Default move-assignment operator */
QXmppRosterIq &QXmppRosterIq::operator=(QXmppRosterIq &&) = default;

/*!
    Adds an \a item to the roster IQ.
*/
void QXmppRosterIq::addItem(const Item &item)
{
    d->items.append(item);
}

/*! Returns the roster IQ's items. */
QList<QXmppRosterIq::Item> QXmppRosterIq::items() const
{
    return d->items;
}

/*!
    Sets the roster IQ's items.

    \since QXmpp 1.8

    \a items.
*/
void QXmppRosterIq::setItems(const QList<Item> &items)
{
    d->items = items;
}

/*!
    Returns the roster version of the IQ.

    \deprecated Use versionOpt() instead, which distinguishes "no \c ver attribute" from
    \c ver="". This getter conflates both cases as an empty string.

    \since QXmpp 1.0
*/
QString QXmppRosterIq::version() const
{
    return d->version.value_or(QString());
}

/*!
    Returns the roster version of the IQ.

    \c std::nullopt means no \c ver attribute was set. An empty string means \c ver="", which
    is how clients advertise RFC 6121 §2.6 roster versioning support to the server.

    \since QXmpp 1.16
*/
std::optional<QString> QXmppRosterIq::versionOpt() const
{
    return d->version;
}

/*!
    Sets the roster \a version of the IQ.

    Pass \c std::nullopt to omit the \c ver attribute. Pass an empty string to emit \c ver="".

    \since QXmpp 1.0 (parameter type changed to std::optional<QString> in QXmpp 1.16)
*/
void QXmppRosterIq::setVersion(std::optional<QString> version)
{
    d->version = std::move(version);
}

/*!
    Returns whether the IQ contains a \c <query> element.

    Roster get/set IQs always have one. An \c <iq type='result'/> from the server in reply to a
    versioned roster get with no \c <query> child means "the cached roster is still up to date";
    this getter is how the roster manager distinguishes that case from an actually empty roster.

    \since QXmpp 1.16
*/
bool QXmppRosterIq::hasQuery() const
{
    return d->hasQuery;
}

/*!
    Sets whether the IQ contains a \c <query> element (\a hasQuery).

    \since QXmpp 1.16
*/
void QXmppRosterIq::setHasQuery(bool hasQuery)
{
    d->hasQuery = hasQuery;
}

/*!
    Returns whether to annotate which items are MIX channels.

    \since QXmpp 1.3
*/
bool QXmppRosterIq::mixAnnotate() const
{
    return d->mixAnnotate;
}

/*!
    Sets whether to include which roster items are MIX channels. This MUST only
    be enabled in get requests.

    \since QXmpp 1.3

    \a mixAnnotate.
*/
void QXmppRosterIq::setMixAnnotate(bool mixAnnotate)
{
    d->mixAnnotate = mixAnnotate;
}

void QXmppRosterIq::parseElementFromChild(const QDomElement &element)
{
    QDomElement queryElement = element.firstChildElement(u"query"_s);
    d->hasQuery = !queryElement.isNull();
    if (queryElement.hasAttribute(u"ver"_s)) {
        d->version = queryElement.attribute(u"ver"_s);
    }
    d->items = parseChildElements<QList<Item>>(queryElement);
    setMixAnnotate(!firstChildElement(queryElement, u"annotate", ns_mix_roster).isNull());
}

void QXmppRosterIq::toXmlElementFromChild(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element {
        PayloadXmlTag,
        // RFC 6121 §2.6: Roster Versioning — emit `ver` whenever explicitly set, including
        // `ver=""` which is how clients advertise versioning support to the server.
        OptionalAttribute { u"ver", d->version },
        // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements
        OptionalContent { d->mixAnnotate, Element { { u"annotate", ns_mix_roster } } },
        d->items,
    });
}

class QXmppRosterIq::ItemPrivate : public QSharedData
{
public:
    ItemPrivate();

    QString bareJid;
    Item::SubscriptionType type;
    QString name;
    // can be subscribe/unsubscribe (attribute "ask")
    QString subscriptionStatus;
    QSet<QString> groups;
    bool approved;
    // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements
    bool isMixChannel = false;
    QString mixParticipantId;
};

QXmppRosterIq::ItemPrivate::ItemPrivate()
    : type(QXmppRosterIq::Item::NotSet),
      approved(false)
{
}

/*! Constructs a new roster entry. */
QXmppRosterIq::Item::Item()
    : d(new ItemPrivate)
{
}

/*! Default copy-constructor */
QXmppRosterIq::Item::Item(const QXmppRosterIq::Item &other) = default;
/*! Default move-constructor */
QXmppRosterIq::Item::Item(QXmppRosterIq::Item &&) = default;
QXmppRosterIq::Item::~Item() = default;
/*! Default assignment operator */
QXmppRosterIq::Item &QXmppRosterIq::Item::operator=(const Item &other) = default;
/*! Default assignment operator */
QXmppRosterIq::Item &QXmppRosterIq::Item::operator=(Item &&) = default;

/*!
    Returns the bareJid of the roster entry as a QString.
*/
QString QXmppRosterIq::Item::bareJid() const
{
    return d->bareJid;
}

/*!
    Sets the \a bareJid of the roster entry.
*/
void QXmppRosterIq::Item::setBareJid(const QString &bareJid)
{
    d->bareJid = bareJid;
}

/*!
    Returns the groups of the roster entry as a QSet<QString> list of all the
    groups.
*/
QSet<QString> QXmppRosterIq::Item::groups() const
{
    return d->groups;
}

/*!
    Sets the \a groups of the roster entry as a QSet<QString> list of all the
    groups.
*/
void QXmppRosterIq::Item::setGroups(const QSet<QString> &groups)
{
    d->groups = groups;
}

/*!
    Returns the name of the roster entry as a QString.
*/
QString QXmppRosterIq::Item::name() const
{
    return d->name;
}

/*!
    Sets the \a name of the roster entry.
*/
void QXmppRosterIq::Item::setName(const QString &name)
{
    d->name = name;
}

/*!
    Returns the subscription status of the roster entry as a QString. It is the
    "ask" attribute in the Roster IQ stanza. Its value can be "subscribe" or
    "unsubscribe" or empty.
*/
QString QXmppRosterIq::Item::subscriptionStatus() const
{
    return d->subscriptionStatus;
}

/*!
    Sets the subscription \a status of the roster entry. It is the "ask"
    attribute in the Roster IQ stanza. Its value can be "subscribe" or
    "unsubscribe" or empty.
*/
void QXmppRosterIq::Item::setSubscriptionStatus(const QString &status)
{
    d->subscriptionStatus = status;
}

/*! Returns the subscription type of the roster entry. */
QXmppRosterIq::Item::SubscriptionType
QXmppRosterIq::Item::subscriptionType() const
{
    return d->type;
}

/*!
    Sets the subscription \a type of the roster entry.
*/
void QXmppRosterIq::Item::setSubscriptionType(SubscriptionType type)
{
    d->type = type;
}

/*!
    Returns whether the item has a pre-approved presence subscription.

    \since QXmpp 1.3
*/
bool QXmppRosterIq::Item::isApproved() const
{
    return d->approved;
}

/*!
    Sets whether the item has a pre-approved presence subscription.

    This cannot be used to initiate a pre-approved subscription. For this
    purpose the client must send a &lt;presence/&gt; stanza of type
    \c subscribed to the user.

    \since QXmpp 1.3

    \a approved.
*/
void QXmppRosterIq::Item::setIsApproved(bool approved)
{
    d->approved = approved;
}

QString QXmppRosterIq::Item::getSubscriptionTypeStr() const
{
    switch (d->type) {
    case NotSet:
        return u""_s;
    case None:
        return u"none"_s;
    case Both:
        return u"both"_s;
    case From:
        return u"from"_s;
    case To:
        return u"to"_s;
    case Remove:
        return u"remove"_s;
    default: {
        qWarning("QXmppRosterIq::Item::getTypeStr(): invalid type");
        return QString();
    }
    }
}

void QXmppRosterIq::Item::setSubscriptionTypeFromStr(const QString &type)
{
    if (type.isEmpty()) {
        setSubscriptionType(NotSet);
    } else if (type == u"none") {
        setSubscriptionType(None);
    } else if (type == u"both") {
        setSubscriptionType(Both);
    } else if (type == u"from") {
        setSubscriptionType(From);
    } else if (type == u"to") {
        setSubscriptionType(To);
    } else if (type == u"remove") {
        setSubscriptionType(Remove);
    } else {
        qWarning("QXmppRosterIq::Item::setTypeFromStr(): invalid type");
    }
}

/*!
    Returns whether this is a MIX channel.

    \since QXmpp 1.3
*/
bool QXmppRosterIq::Item::isMixChannel() const
{
    return d->isMixChannel;
}

/*!
    Sets whether this is a MIX channel.

    \since QXmpp 1.3

    \a isMixChannel.
*/
void QXmppRosterIq::Item::setIsMixChannel(bool isMixChannel)
{
    d->isMixChannel = isMixChannel;
}

/*!
    Returns the participant id for this MIX channel.

    \since QXmpp 1.3
*/
QString QXmppRosterIq::Item::mixParticipantId() const
{
    return d->mixParticipantId;
}

/*!
    Sets the participant id for this MIX channel.

    \since QXmpp 1.3

    \a participantId.
*/
void QXmppRosterIq::Item::setMixParticipantId(const QString &participantId)
{
    d->mixParticipantId = participantId;
}

void QXmppRosterIq::Item::parse(const QDomElement &element)
{
    d->name = element.attribute(u"name"_s);
    d->bareJid = element.attribute(u"jid"_s);
    setSubscriptionTypeFromStr(element.attribute(u"subscription"_s));
    setSubscriptionStatus(element.attribute(u"ask"_s));

    // pre-approved
    const QString approved = element.attribute(u"approved"_s);
    d->approved = (approved == u"1" || approved == u"true");

    d->groups = parseTextElements<QSet<QString>>(element, u"group", ns_roster);

    // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements
    auto channelElement = firstChildElement(element, u"channel", ns_mix_roster);
    if (!channelElement.isNull()) {
        d->isMixChannel = true;
        d->mixParticipantId = channelElement.attribute(u"participant-id"_s);
    }
}

void QXmppRosterIq::Item::toXml(QXmlStreamWriter *writer) const
{
    toXml(writer, false);
}

void QXmppRosterIq::Item::toXml(QXmlStreamWriter *writer, bool external) const
{
    XmlWriter(writer).write(Element {
        u"item",
        OptionalContent { external, DefaultNamespace { ns_roster } },
        Attribute { u"jid", d->bareJid },
        OptionalAttribute { u"name", d->name },
        OptionalAttribute { u"subscription", getSubscriptionTypeStr() },
        OptionalAttribute { u"ask", d->subscriptionStatus },
        OptionalAttribute { u"approved", DefaultedBool { d->approved, false } },
        TextElements { u"group", d->groups },
        // XEP-0405: Mediated Information eXchange (MIX): Participant Server Requirements
        OptionalContent {
            d->isMixChannel,
            Element {
                { u"channel", ns_mix_roster },
                OptionalAttribute { u"participant-id", d->mixParticipantId },
            },
        },
    });
}
