// SPDX-FileCopyrightText: 2010 Jeremy Lainé <jeremy.laine@m4x.org>
// SPDX-FileCopyrightText: 2022 Melvin Keskin <melvo@olomono.de>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppJingleData.h"

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

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

#include <QDate>
#include <QDateTime>
#include <QDomElement>
#include <QRegularExpression>

using namespace QXmpp::Private;

static const int RTP_COMPONENT = 1;

template<>
struct Enums::Data<QXmppJingleIq::Action> {
    using enum QXmppJingleIq::Action;
    static constexpr auto Values = makeValues<QXmppJingleIq::Action>({
        { ContentAccept, u"content-accept" },
        { ContentAdd, u"content-add" },
        { ContentModify, u"content-modify" },
        { ContentReject, u"content-reject" },
        { ContentRemove, u"content-remove" },
        { DescriptionInfo, u"description-info" },
        { SecurityInfo, u"security-info" },
        { SessionAccept, u"session-accept" },
        { SessionInfo, u"session-info" },
        { SessionInitiate, u"session-initiate" },
        { SessionTerminate, u"session-terminate" },
        { TransportAccept, u"transport-accept" },
        { TransportInfo, u"transport-info" },
        { TransportReject, u"transport-reject" },
        { TransportReplace, u"transport-replace" },
    });
};

template<>
struct Enums::Data<QXmppJingleReason::Type> {
    using enum QXmppJingleReason::Type;
    static constexpr auto Values = makeValues<QXmppJingleReason::Type>({
        { None, {} },
        { AlternativeSession, u"alternative-session" },
        { Busy, u"busy" },
        { Cancel, u"cancel" },
        { ConnectivityError, u"connectivity-error" },
        { Decline, u"decline" },
        { Expired, u"expired" },
        { FailedApplication, u"failed-application" },
        { FailedTransport, u"failed-transport" },
        { GeneralError, u"general-error" },
        { Gone, u"gone" },
        { IncompatibleParameters, u"incompatible-parameters" },
        { MediaError, u"media-error" },
        { SecurityError, u"security-error" },
        { Success, u"success" },
        { Timeout, u"timeout" },
        { UnsupportedApplications, u"unsupported-applications" },
        { UnsupportedTransports, u"unsupported-transports" },
    });
};

template<>
struct Enums::Data<QXmppJingleReason::RtpErrorCondition> {
    using enum QXmppJingleReason::RtpErrorCondition;
    static constexpr auto NullValue = NoErrorCondition;
    static constexpr auto Values = makeValues<QXmppJingleReason::RtpErrorCondition>({
        { NoErrorCondition, {} },
        { InvalidCrypto, u"invalid-crypto" },
        { CryptoRequired, u"crypto-required" },
    });
};

template<>
struct Enums::Data<QXmppJingleRtpHeaderExtensionProperty::Senders> {
    using enum QXmppJingleRtpHeaderExtensionProperty::Senders;
    static constexpr auto Values = makeValues<QXmppJingleRtpHeaderExtensionProperty::Senders>({
        { Both, u"both" },
        { Initiator, u"initiator" },
        { Responder, u"responder" },
    });
};

template<>
struct Enums::Data<QXmppJingleCandidate::Type> {
    using enum QXmppJingleCandidate::Type;
    static constexpr auto Values = makeValues<QXmppJingleCandidate::Type>({
        { HostType, u"host" },
        { PeerReflexiveType, u"prflx" },
        { ServerReflexiveType, u"srflx" },
        { RelayedType, u"relay" },
    });
};

template<>
struct Enums::Data<QXmppJingleMessageInitiationElement::Type> {
    using enum QXmppJingleMessageInitiationElement::Type;
    static constexpr auto Values = makeValues<QXmppJingleMessageInitiationElement::Type>({
        { None, {} },
        { Propose, u"propose" },
        { Ringing, u"ringing" },
        { Proceed, u"proceed" },
        { Reject, u"reject" },
        { Retract, u"retract" },
        { Finish, u"finish" },
    });
};

template<>
struct Enums::Data<QXmppCallInviteElement::Type> {
    using enum QXmppCallInviteElement::Type;
    static constexpr auto Values = makeValues<QXmppCallInviteElement::Type>({
        { None, {} },
        { Invite, u"invite" },
        { Retract, u"retract" },
        { Accept, u"accept" },
        { Reject, u"reject" },
        { Left, u"left" },
    });
};

static QString formatFingerprint(const QByteArray &digest)
{
    QString fingerprint;
    const QString hx = QString::fromUtf8(digest.toHex()).toUpper();
    for (int i = 0; i < hx.size(); i += 2) {
        if (!fingerprint.isEmpty()) {
            fingerprint += u':';
        }
        fingerprint += hx.mid(i, 2);
    }
    return fingerprint;
}

static QByteArray parseFingerprint(const QString &fingerprint)
{
    QString z = fingerprint;
    z.replace(u':', QChar());
    return QByteArray::fromHex(z.toUtf8());
}

static QString addressToSdp(const QHostAddress &host)
{
    return u"IN %1 %2"_s.arg(host.protocol() == QAbstractSocket::IPv6Protocol ? u"IP6"_s : u"IP4"_s, host.toString());
}

static bool candidateParseSdp(QXmppJingleCandidate *candidate, const QString &sdp)
{
    if (!sdp.startsWith(u"candidate:"_s)) {
        return false;
    }

    const QStringList bits = sdp.mid(10).split(u' ');
    if (bits.size() < 6) {
        return false;
    }

    candidate->setFoundation(bits[0]);
    candidate->setComponent(bits[1].toInt());
    candidate->setProtocol(bits[2].toLower());
    candidate->setPriority(bits[3].toInt());
    candidate->setHost(QHostAddress(bits[4]));
    candidate->setPort(bits[5].toInt());
    for (int i = 6; i < bits.size() - 1; i += 2) {
        if (bits[i] == u"typ") {
            if (auto type = Enums::fromString<QXmppJingleCandidate::Type>(bits[i + 1])) {
                candidate->setType(type.value());
            } else {
                return false;
            }
        } else if (bits[i] == u"generation") {
            candidate->setGeneration(bits[i + 1].toInt());
        } else {
            qWarning() << "Candidate SDP contains unknown attribute" << bits[i];
            return false;
        }
    }
    return true;
}

static QString candidateToSdp(const QXmppJingleCandidate &candidate)
{
    return QStringView(u"candidate:%1 %2 %3 %4 %5 %6 typ %7 generation %8")
        .arg(candidate.foundation(),
             QString::number(candidate.component()),
             candidate.protocol(),
             QString::number(candidate.priority()),
             candidate.host().toString(),
             QString::number(candidate.port()),
             Enums::toString(candidate.type()),
             QString::number(candidate.generation()));
}

class QXmppJingleIqContentPrivate : public QSharedData
{
public:
    QXmppJingleIqContentPrivate();

    QString creator;
    QString disposition;
    QString name;
    QString senders;

    QXmppJingleRtpDescription description;
    bool isRtpMultiplexingSupported = false;

    QString transportType;
    QString transportUser;
    QString transportPassword;

    QByteArray transportFingerprint;
    QString transportFingerprintHash;
    QString transportFingerprintSetup;

    QList<QXmppJingleCandidate> transportCandidates;

    // XEP-0167: Jingle RTP Sessions
    std::optional<QXmppJingleRtpEncryption> rtpEncryption;

    // XEP-0293: Jingle RTP Feedback Negotiation
    QList<QXmppJingleRtpFeedbackProperty> rtpFeedbackProperties;
    QList<QXmppJingleRtpFeedbackInterval> rtpFeedbackIntervals;

    // XEP-0294: Jingle RTP Header Extensions Negotiation
    QList<QXmppJingleRtpHeaderExtensionProperty> rtpHeaderExtensionProperties;
    bool isRtpHeaderExtensionMixingAllowed = false;
};

QXmppJingleIqContentPrivate::QXmppJingleIqContentPrivate()
{
    description.setSsrc(0);
}

/*!
    \enum QXmppJingleIq::Creator

    Party that originally generated the content type.

    \value Initiator The initiator generated the content type.
    \value Responder The responder generated the content type.

    \since QXmpp 1.5
*/

/*!
    \struct QXmppJingleIq::RtpSessionStateActive
    \inmodule QXmpp

    Actively participating in the session after having been on mute or having put the other party on
    hold

    \since QXmpp 1.5
*/

/*!
    \struct QXmppJingleIq::RtpSessionStateHold
    \inmodule QXmpp

    Temporarily not listening for media from the other party

    \since QXmpp 1.5
*/

/*!
    \struct QXmppJingleIq::RtpSessionStateUnhold
    \inmodule QXmpp

    Ending hold state

    \since QXmpp 1.5
*/

/*!
    \struct QXmppJingleIq::RtpSessionStateMuting
    \inmodule QXmpp

    State for muting or unmuting

    \since QXmpp 1.5
*/

/*!
    \struct QXmppJingleIq::RtpSessionStateRinging
    \inmodule QXmpp

    State after the callee acknowledged the call but did not yet interacted with it

    \since QXmpp 1.5
*/

/*!
    \typedef QXmppJingleIq::RtpSessionState

    Contains the state of an RTP session as specified by \xep{0167}{Jingle RTP Sessions}
    Informational Messages.

    \since QXmpp 1.5
*/

/*! Constructs an empty content. */
QXmppJingleIq::Content::Content()
    : d(new QXmppJingleIqContentPrivate())
{
}

/*! Copy-constructor. */
QXmppJingleIq::Content::Content(const QXmppJingleIq::Content &other) = default;
/*! Move-constructor. */
QXmppJingleIq::Content::Content(QXmppJingleIq::Content &&) = default;
/*! Assignment operator. */
QXmppJingleIq::Content &QXmppJingleIq::Content::operator=(const QXmppJingleIq::Content &) = default;
/*! Move-assignment operator. */
QXmppJingleIq::Content &QXmppJingleIq::Content::operator=(QXmppJingleIq::Content &&) = default;

QXmppJingleIq::Content::~Content() = default;

QString QXmppJingleIq::Content::creator() const
{
    return d->creator;
}

void QXmppJingleIq::Content::setCreator(const QString &creator)
{
    d->creator = creator;
}

QString QXmppJingleIq::Content::name() const
{
    return d->name;
}

void QXmppJingleIq::Content::setName(const QString &name)
{
    d->name = name;
}

QString QXmppJingleIq::Content::senders() const
{
    return d->senders;
}

void QXmppJingleIq::Content::setSenders(const QString &senders)
{
    d->senders = senders;
}

/*!
    Returns the description as specified by
    \xep{0167}{Jingle RTP Sessions} and RFC 3550.

    \since QXmpp 0.9
*/
QXmppJingleRtpDescription QXmppJingleIq::Content::description() const
{
    return d->description;
}

void QXmppJingleIq::Content::setDescription(const QXmppJingleRtpDescription &description)
{
    d->description = description;
}

/*!
    \deprecated This method is deprecated since QXmpp 1.6. Use
    \c QXmppJingleIq::Conent::description().media() instead.
*/
QString QXmppJingleIq::Content::descriptionMedia() const
{
    return d->description.media();
}

/*!
    \deprecated This method is deprecated since QXmpp 1.6. Use
    \c QXmppJingleIq::Conent::description().setMedia() instead.
*/
void QXmppJingleIq::Content::setDescriptionMedia(const QString &media)
{
    d->description.setMedia(media);
}

/*!
    Returns the description's 32-bit synchronization source for the media stream as specified by
    \xep{0167}{Jingle RTP Sessions} and RFC 3550.

    \since QXmpp 0.9
    \deprecated This method is deprecated since QXmpp 1.6. Use
    \c QXmppJingleIq::Content::description().setSsrc() instead.
*/
quint32 QXmppJingleIq::Content::descriptionSsrc() const
{
    return d->description.ssrc();
}

/*!
    \deprecated This method is deprecated since QXmpp 1.6. Use
    \c QXmppJingleIq::Conent::description().setSsrc() instead.
*/
void QXmppJingleIq::Content::setDescriptionSsrc(quint32 ssrc)
{
    d->description.setSsrc(ssrc);
}

/*!
    \deprecated This method is deprecated since QXmpp 1.6. Use
    \c QXmppJingleIq::Conent::description().addPayloadType() instead.
*/
void QXmppJingleIq::Content::addPayloadType(const QXmppJinglePayloadType &payload)
{
    d->description.addPayloadType(payload);
}

/*!
    \deprecated This method is deprecated since QXmpp 1.6. Use
    \c QXmppJingleIq::Conent::description().payloadTypes() instead.
*/
QList<QXmppJinglePayloadType> QXmppJingleIq::Content::payloadTypes() const
{
    return d->description.payloadTypes();
}

/*!
    \deprecated This method is deprecated since QXmpp 1.6. Use
    \c QXmppJingleIq::Conent::description().setPayloadTypes() instead.
*/
void QXmppJingleIq::Content::setPayloadTypes(const QList<QXmppJinglePayloadType> &payloadTypes)
{
    d->description.setPayloadTypes(payloadTypes);
}

/*!
    Returns whether multiplexing of RTP data and control packets on a single port is supported as
    specified by \xep{0167}{Jingle RTP Sessions} and  RFC 5761.

    \since QXmpp 1.5
*/
bool QXmppJingleIq::Content::isRtpMultiplexingSupported() const
{
    return d->isRtpMultiplexingSupported;
}

/*!
    Sets via \a isRtpMultiplexingSupported whether multiplexing of RTP data and
    control packets on a single port is supported as specified by \xepn{0167}
    (Jingle RTP Sessions) and RFC 5761.

    \since QXmpp 1.5
*/
void QXmppJingleIq::Content::setRtpMultiplexingSupported(bool isRtpMultiplexingSupported)
{
    d->isRtpMultiplexingSupported = isRtpMultiplexingSupported;
}

/*!
    Returns the RTP encryption via SRTP used for SRTP negotiation as specified
    by \xep{0167}{Jingle RTP Sessions}.

    \since QXmpp 1.5
*/
std::optional<QXmppJingleRtpEncryption> QXmppJingleIq::Content::rtpEncryption() const
{
    return d->rtpEncryption;
}

/*!
    Sets the \a rtpEncryption (RTP encryption via SRTP) used for SRTP
    negotiation as specified by \xep{0167}{Jingle RTP Sessions}.

    \since QXmpp 1.5
*/
void QXmppJingleIq::Content::setRtpEncryption(const std::optional<QXmppJingleRtpEncryption> &rtpEncryption)
{
    d->rtpEncryption = rtpEncryption;
}

void QXmppJingleIq::Content::addTransportCandidate(const QXmppJingleCandidate &candidate)
{
    d->transportType = staticString(ns_jingle_ice_udp);
    d->transportCandidates << candidate;
}

QList<QXmppJingleCandidate> QXmppJingleIq::Content::transportCandidates() const
{
    return d->transportCandidates;
}

/*!
    Sets a list of transport candidates.

    \since QXmpp 0.9.2
*/
void QXmppJingleIq::Content::setTransportCandidates(const QList<QXmppJingleCandidate> &candidates)
{
    d->transportType = candidates.isEmpty() ? QString() : staticString(ns_jingle_ice_udp);
    d->transportCandidates = candidates;
}

QString QXmppJingleIq::Content::transportUser() const
{
    return d->transportUser;
}

void QXmppJingleIq::Content::setTransportUser(const QString &user)
{
    d->transportUser = user;
}

QString QXmppJingleIq::Content::transportPassword() const
{
    return d->transportPassword;
}

void QXmppJingleIq::Content::setTransportPassword(const QString &password)
{
    d->transportPassword = password;
}

/*!
    Returns the RTP feedback properties.

    \since QXmpp 1.5
*/
QList<QXmppJingleRtpFeedbackProperty> QXmppJingleIq::Content::rtpFeedbackProperties() const
{
    return d->rtpFeedbackProperties;
}

/*!
    Sets the \a rtpFeedbackProperties of RTP feedback.

    \since QXmpp 1.5
*/
void QXmppJingleIq::Content::setRtpFeedbackProperties(const QList<QXmppJingleRtpFeedbackProperty> &rtpFeedbackProperties)
{
    d->rtpFeedbackProperties = rtpFeedbackProperties;
}

/*!
    Returns the RTP feedback intervals.

    \since QXmpp 1.5
*/
QList<QXmppJingleRtpFeedbackInterval> QXmppJingleIq::Content::rtpFeedbackIntervals() const
{
    return d->rtpFeedbackIntervals;
}

/*!
    Sets the \a rtpFeedbackIntervals of RTP feedback.

    \since QXmpp 1.5
*/
void QXmppJingleIq::Content::setRtpFeedbackIntervals(const QList<QXmppJingleRtpFeedbackInterval> &rtpFeedbackIntervals)
{
    d->rtpFeedbackIntervals = rtpFeedbackIntervals;
}

/*!
    Returns the RTP header extension properties.

    \since QXmpp 1.5
*/
QList<QXmppJingleRtpHeaderExtensionProperty> QXmppJingleIq::Content::rtpHeaderExtensionProperties() const
{
    return d->rtpHeaderExtensionProperties;
}

/*!
    Sets the \a rtpHeaderExtensionProperties.

    \since QXmpp 1.5
*/
void QXmppJingleIq::Content::setRtpHeaderExtensionProperties(const QList<QXmppJingleRtpHeaderExtensionProperty> &rtpHeaderExtensionProperties)
{
    d->rtpHeaderExtensionProperties = rtpHeaderExtensionProperties;
}

/*!
    Returns whether mixing of RTP header extensions is allowed corresponding to the
    "extmap-allow-mixed" element as specified by
    \xep{0293}{Jingle RTP Header Extensions Negotiation}.

    \since QXmpp 1.5
*/
bool QXmppJingleIq::Content::isRtpHeaderExtensionMixingAllowed() const
{
    return d->isRtpHeaderExtensionMixingAllowed;
}

/*!
    Sets via \a isRtpHeaderExtensionMixingAllowed whether mixing of RTP header
    extensions is allowed corresponding to the "extmap-allow-mixed" element as
    specified by \xep{0293}{Jingle RTP Header Extensions Negotiation}.

    \since QXmpp 1.5
*/
void QXmppJingleIq::Content::setRtpHeaderExtensionMixingAllowed(bool isRtpHeaderExtensionMixingAllowed)
{
    d->isRtpHeaderExtensionMixingAllowed = isRtpHeaderExtensionMixingAllowed;
}

/*!
    Returns the fingerprint hash value for the transport key.

    This is used for DTLS-SRTP as defined in \xep{0320}{Use of DTLS-SRTP in Jingle Sessions}.

    \since QXmpp 0.9
*/
QByteArray QXmppJingleIq::Content::transportFingerprint() const
{
    return d->transportFingerprint;
}

/*!
    Sets the fingerprint hash value for the transport key.

    This is used for DTLS-SRTP as defined in \xep{0320}{Use of DTLS-SRTP in Jingle Sessions}.

    \since QXmpp 0.9
*/
void QXmppJingleIq::Content::setTransportFingerprint(const QByteArray &fingerprint)
{
    d->transportFingerprint = fingerprint;
}

/*!
    Returns the fingerprint hash algorithm for the transport key.

    This is used for DTLS-SRTP as defined in \xep{0320}{Use of DTLS-SRTP in Jingle Sessions}.

    \since QXmpp 0.9
*/
QString QXmppJingleIq::Content::transportFingerprintHash() const
{
    return d->transportFingerprintHash;
}

/*!
    Sets the fingerprint hash algorithm for the transport key.

    This is used for DTLS-SRTP as defined in \xep{0320}{Use of DTLS-SRTP in Jingle Sessions}.

    \since QXmpp 0.9
*/
void QXmppJingleIq::Content::setTransportFingerprintHash(const QString &hash)
{
    d->transportFingerprintHash = hash;
}

/*!
    Returns the setup role for the encrypted transport.

    This is used for DTLS-SRTP as defined in \xep{0320}{Use of DTLS-SRTP in Jingle Sessions}.

    \since QXmpp 0.9
*/
QString QXmppJingleIq::Content::transportFingerprintSetup() const
{
    return d->transportFingerprintSetup;
}

/*!
    Sets the setup role for the encrypted transport.

    This is used for DTLS-SRTP as defined in \xep{0320}{Use of DTLS-SRTP in Jingle Sessions}.

    \since QXmpp 0.9
*/
void QXmppJingleIq::Content::setTransportFingerprintSetup(const QString &setup)
{
    d->transportFingerprintSetup = setup;
}

void QXmppJingleIq::Content::parse(const QDomElement &element)
{
    d->creator = element.attribute(u"creator"_s);
    d->disposition = element.attribute(u"disposition"_s);
    d->name = element.attribute(u"name"_s);
    d->senders = element.attribute(u"senders"_s);

    // description
    QDomElement descriptionElement = element.firstChildElement(u"description"_s);
    d->description.setMedia(descriptionElement.attribute(u"media"_s));
    d->description.setSsrc(parseInt<uint32_t>(descriptionElement.attribute(u"ssrc"_s)).value_or(0));
    d->isRtpMultiplexingSupported = !descriptionElement.firstChildElement(u"rtcp-mux"_s).isNull();
    d->rtpEncryption = parseElement<QXmppJingleRtpEncryption>(firstChildElement(descriptionElement, u"encryption", ns_jingle_rtp));
    d->rtpFeedbackProperties = parseChildElements<QList<QXmppJingleRtpFeedbackProperty>>(descriptionElement);
    d->rtpFeedbackIntervals = parseChildElements<QList<QXmppJingleRtpFeedbackInterval>>(descriptionElement);
    d->rtpHeaderExtensionProperties = parseChildElements<QList<QXmppJingleRtpHeaderExtensionProperty>>(descriptionElement);
    d->isRtpHeaderExtensionMixingAllowed = !firstChildElement(descriptionElement, u"extmap-allow-mixed", ns_jingle_rtp_hdrext).isNull();
    d->description.setPayloadTypes(parseChildElements<QList<QXmppJinglePayloadType>>(descriptionElement));

    // transport
    QDomElement transportElement = element.firstChildElement(u"transport"_s);
    d->transportType = transportElement.namespaceURI();
    d->transportUser = transportElement.attribute(u"ufrag"_s);
    d->transportPassword = transportElement.attribute(u"pwd"_s);
    d->transportCandidates = parseChildElements<QList<QXmppJingleCandidate>>(transportElement);

    // XEP-0320
    auto child = firstChildElement(transportElement, u"fingerprint");
    if (!child.isNull()) {
        d->transportFingerprint = parseFingerprint(child.text());
        d->transportFingerprintHash = child.attribute(u"hash"_s);
        d->transportFingerprintSetup = child.attribute(u"setup"_s);
    }
}

void QXmppJingleIq::Content::toXml(QXmlStreamWriter *writer) const
{
    if (d->creator.isEmpty() || d->name.isEmpty()) {
        return;
    }

    XmlWriter w(writer);
    w.write(Element {
        u"content",
        OptionalAttribute { u"creator", d->creator },
        OptionalAttribute { u"disposition", d->disposition },
        OptionalAttribute { u"name", d->name },
        OptionalAttribute { u"senders", d->senders },
        // description
        OptionalContent {
            !d->description.payloadTypes().isEmpty(),
            Element {
                { u"description", ns_jingle_rtp },
                OptionalAttribute { u"media", d->description.media() },
                OptionalContent { d->description.ssrc() != 0, Attribute { u"ssrc", d->description.ssrc() } },
                OptionalContent { d->isRtpMultiplexingSupported, Element { u"rtcp-mux" } },
                d->rtpEncryption,
                d->rtpFeedbackProperties,
                d->rtpFeedbackIntervals,
                d->rtpHeaderExtensionProperties,
                OptionalContent { d->isRtpHeaderExtensionMixingAllowed, Element { { u"extmap-allow-mixed", ns_jingle_rtp_hdrext } } },
                d->description.payloadTypes(),
            },
        },
        // transport
        OptionalContent {
            !d->transportType.isEmpty() || !d->transportCandidates.isEmpty(),
            Element {
                { u"transport", d->transportType },
                OptionalAttribute { u"ufrag", d->transportUser },
                OptionalAttribute { u"pwd", d->transportPassword },
                d->transportCandidates,
                // XEP-0320: DTLS-SRTP
                [&] {
                    if (!d->transportFingerprint.isEmpty() && !d->transportFingerprintHash.isEmpty()) {
                        w.write(Element {
                            { u"fingerprint", ns_jingle_dtls },
                            Attribute { u"hash", d->transportFingerprintHash },
                            Attribute { u"setup", d->transportFingerprintSetup },
                            Characters { formatFingerprint(d->transportFingerprint) },
                        });
                    }
                },
            },
        },
    });
}

bool QXmppJingleIq::Content::parseSdp(const QString &sdp)
{
    QList<QXmppJinglePayloadType> payloads;
    for (auto &line : sdp.split(QChar(u'\n'))) {
        if (line.endsWith(u'\r')) {
            line.resize(line.size() - 1);
        }
        if (line.startsWith(u"a="_s)) {
            qsizetype idx = line.indexOf(u':');
            const QString attrName = idx != -1 ? line.mid(2, idx - 2) : line.mid(2);
            const QString attrValue = idx != -1 ? line.mid(idx + 1) : QString();

            if (attrName == u"candidate") {
                QXmppJingleCandidate candidate;
                if (!candidateParseSdp(&candidate, line.mid(2))) {
                    qWarning() << "Could not parse candidate" << line;
                    return false;
                }
                addTransportCandidate(candidate);
            } else if (attrName == u"fingerprint") {
                const QStringList bits = attrValue.split(u' ');
                if (bits.size() > 1) {
                    d->transportFingerprintHash = bits[0];
                    d->transportFingerprint = parseFingerprint(bits[1]);
                }
            } else if (attrName == u"fmtp") {
                qsizetype spIdx = attrValue.indexOf(u' ');
                if (spIdx == -1) {
                    qWarning() << "Could not parse payload parameters" << line;
                    return false;
                }
                const int id = attrValue.left(spIdx).toInt();
                const QString paramStr = attrValue.mid(spIdx + 1);
                for (auto &payload : payloads) {
                    if (payload.id() == id) {
                        QMap<QString, QString> params;
                        if (payload.name() == u"telephone-event") {
                            params.insert(u"events"_s, paramStr);
                        } else {
                            thread_local static const auto regex = QRegularExpression(u";\\s*"_s);
                            const auto paramParts = paramStr.split(regex);
                            for (const auto &p : paramParts) {
                                const QStringList bits = p.split(u'=');
                                if (bits.size() == 2) {
                                    params.insert(bits.at(0), bits.at(1));
                                }
                            }
                        }
                        payload.setParameters(params);
                    }
                }
            } else if (attrName == u"rtpmap") {
                // payload type map
                const QStringList bits = attrValue.split(u' ');
                if (bits.size() != 2) {
                    continue;
                }
                bool ok = false;
                const int id = bits[0].toInt(&ok);
                if (!ok) {
                    continue;
                }

                const QStringList args = bits[1].split(u'/');
                for (auto &payload : payloads) {
                    if (payload.id() == id) {
                        payload.setName(args[0]);
                        if (args.size() > 1) {
                            payload.setClockrate(args[1].toUInt());
                        }
                        if (args.size() > 2) {
                            payload.setChannels(parseInt<uint8_t>(args[2]).value_or(0));
                        }
                    }
                }
            } else if (attrName == u"ice-ufrag") {
                d->transportUser = attrValue;
            } else if (attrName == u"ice-pwd") {
                d->transportPassword = attrValue;
            } else if (attrName == u"setup") {
                d->transportFingerprintSetup = attrValue;
            } else if (attrName == u"ssrc") {
                const QStringList bits = attrValue.split(u' ');
                if (bits.isEmpty()) {
                    qWarning() << "Could not parse ssrc" << line;
                    return false;
                }
                d->description.setSsrc(bits[0].toULong());
            }
        } else if (line.startsWith(u"m="_s)) {
            // FIXME: what do we do with the profile (bits[2]) ?
            QStringList bits = line.mid(2).split(u' ');
            if (bits.size() < 3) {
                qWarning() << "Could not parse media" << line;
                return false;
            }
            d->description.setMedia(bits[0]);

            // parse payload types
            for (int i = 3; i < bits.size(); ++i) {
                bool ok = false;
                int id = bits[i].toInt(&ok);
                if (!ok) {
                    continue;
                }
                QXmppJinglePayloadType payload;
                payload.setId(id);
                payloads << payload;
            }
        }
    }

    d->description.setPayloadTypes(payloads);
    return true;
}

static bool candidateLessThan(const QXmppJingleCandidate &c1, const QXmppJingleCandidate &c2)
{
    if (c1.type() == c2.type()) {
        return c1.priority() > c2.priority();
    } else {
        return c1.type() == QXmppJingleCandidate::ServerReflexiveType;
    }
}

QString QXmppJingleIq::Content::toSdp() const
{
    // get default candidate
    QHostAddress localRtpAddress = QHostAddress::Any;
    quint16 localRtpPort = 0;
    QList<QXmppJingleCandidate> sortedCandidates = d->transportCandidates;
    std::sort(sortedCandidates.begin(), sortedCandidates.end(), candidateLessThan);
    for (const auto &candidate : sortedCandidates) {
        if (candidate.component() == RTP_COMPONENT) {
            localRtpAddress = candidate.host();
            localRtpPort = candidate.port();
            break;
        }
    }

    QStringList sdp;

    // media
    QString payloads;
    QStringList attrs;
    for (const QXmppJinglePayloadType &payload : d->description.payloadTypes()) {
        payloads += u" ";
        payloads += QString::number(payload.id());
        QString rtpmap = QString::number(payload.id()) + u' ' + payload.name() + u'/' + QString::number(payload.clockrate());
        if (payload.channels() > 1) {
            rtpmap += u'/' + QString::number(payload.channels());
        }
        attrs << u"a=rtpmap:" + rtpmap;

        // payload parameters
        QStringList paramList;
        const QMap<QString, QString> params = payload.parameters();
        if (payload.name() == u"telephone-event") {
            if (params.contains(u"events"_s)) {
                paramList << params.value(u"events"_s);
            }
        } else {
            QMap<QString, QString>::const_iterator i;
            for (i = params.begin(); i != params.end(); ++i) {
                paramList << i.key() + u"="_s + i.value();
            }
        }
        if (!paramList.isEmpty()) {
            attrs << QString(u"a=fmtp:" + QString::number(payload.id()) + u' ' + paramList.join(u"; "_s));
        }
    }
    sdp << u"m=%1 %2 RTP/AVP%3"_s.arg(d->description.media(), QString::number(localRtpPort), payloads);
    sdp << u"c=%1"_s.arg(addressToSdp(localRtpAddress));
    sdp += attrs;

    // transport
    for (const auto &candidate : d->transportCandidates) {
        sdp << u"a=%1"_s.arg(candidateToSdp(candidate));
    }
    if (!d->transportUser.isEmpty()) {
        sdp << u"a=ice-ufrag:%1"_s.arg(d->transportUser);
    }
    if (!d->transportPassword.isEmpty()) {
        sdp << u"a=ice-pwd:%1"_s.arg(d->transportPassword);
    }
    if (!d->transportFingerprint.isEmpty() && !d->transportFingerprintHash.isEmpty()) {
        sdp << u"a=fingerprint:%1 %2"_s.arg(d->transportFingerprintHash, formatFingerprint(d->transportFingerprint));
    }
    if (!d->transportFingerprintSetup.isEmpty()) {
        sdp << u"a=setup:%1"_s.arg(d->transportFingerprintSetup);
    }

    return sdp.join(u"\r\n") + u"\r\n";
}

class QXmppJingleIqReasonPrivate : public QSharedData
{
public:
    QString m_text;
    QXmppJingleReason::Type m_type = QXmppJingleReason::None;
    QXmppJingleReason::RtpErrorCondition m_rtpErrorCondition = QXmppJingleReason::NoErrorCondition;
};

/*!
    \class QXmppJingleReason
    \inmodule QXmpp

    The QXmppJingleReason class represents the "reason" element of a
    QXmppJingle element.
*/

/*! Constructs an empty QXmppJingleReason. */
QXmppJingleReason::QXmppJingleReason()
    : d(new QXmppJingleIqReasonPrivate())
{
}

/*!
    Constructs a QXmppJingleReason with specific type, error text and RTP error condition.

    \since QXmpp 1.11
*/
QXmppJingleReason::QXmppJingleReason(Type type, const QString &text, std::optional<RtpErrorCondition> rtpError)
    : d(new QXmppJingleIqReasonPrivate { {}, text, type, rtpError.value_or(NoErrorCondition) })
{
}

/*! Returns the reason's textual description. */
QString QXmppJingleReason::text() const
{
    return d->m_text;
}

/*!
    Sets the reason's textual description.

    \a text.
*/
void QXmppJingleReason::setText(const QString &text)
{
    d->m_text = text;
}

/*! Gets the reason's type. */
QXmppJingleReason::Type QXmppJingleReason::type() const
{
    return d->m_type;
}

/*! Sets the reason's \a type. */
void QXmppJingleReason::setType(QXmppJingleReason::Type type)
{
    d->m_type = type;
}

/*!
    Returns the RTP error condition as specified by \xep{0167}{Jingle RTP Sessions}.

    \since QXmpp 1.5
*/
QXmppJingleReason::RtpErrorCondition QXmppJingleReason::rtpErrorCondition() const
{
    return d->m_rtpErrorCondition;
}

/*!
    Sets the \a rtpErrorCondition as specified by \xep{0167}{Jingle RTP Sessions}.

    \since QXmpp 1.5
*/
void QXmppJingleReason::setRtpErrorCondition(RtpErrorCondition rtpErrorCondition)
{
    d->m_rtpErrorCondition = rtpErrorCondition;
}

void QXmppJingleReason::parse(const QDomElement &element)
{
    // the reason type element must be the first child element
    auto reasonTypeEl = element.firstChildElement();
    if (reasonTypeEl.namespaceURI() != ns_jingle) {
        return;
    }
    // actually 'None' would also be an error: type is required
    d->m_type = Enums::fromString<Type>(reasonTypeEl.tagName()).value_or(None);
    d->m_text = element.firstChildElement(u"text"_s).text();
    auto child = firstChildElement(element, {}, ns_jingle_rtp_errors);
    d->m_rtpErrorCondition = Enums::fromString<RtpErrorCondition>(child.tagName())
                                 .value_or(NoErrorCondition);
}

void QXmppJingleReason::toXml(QXmlStreamWriter *writer) const
{
    if (d->m_type < AlternativeSession || d->m_type > UnsupportedTransports) {
        return;
    }

    XmlWriter(writer).write(Element {
        XmlTag,
        Element { d->m_type },
        OptionalTextElement { u"text", d->m_text },
        OptionalEnumElement { d->m_rtpErrorCondition, ns_jingle_rtp_errors },
    });
}

class QXmppJingleIqPrivate : public QSharedData
{
public:
    QXmppJingleIqPrivate();

    QXmppJingleIq::Action action;
    QString initiator;
    QString responder;
    QString sid;

    QString mujiGroupChatJid;

    QList<QXmppJingleIq::Content> contents;

    // not a std::optional because of deprecated T &reason() getter
    bool actionReasonSet = false;
    QXmppJingleReason actionReason;

    std::optional<QXmppJingleIq::RtpSessionState> rtpSessionState;
};

QXmppJingleIqPrivate::QXmppJingleIqPrivate()
    : action(QXmppJingleIq::ContentAccept)
{
}

/*! Constructs a QXmppJingleIq. */
QXmppJingleIq::QXmppJingleIq()
    : d(new QXmppJingleIqPrivate())
{
}

/*! Copy-constructor. */
QXmppJingleIq::QXmppJingleIq(const QXmppJingleIq &) = default;
/*! Move-constructor. */
QXmppJingleIq::QXmppJingleIq(QXmppJingleIq &&) = default;

QXmppJingleIq::~QXmppJingleIq() = default;

/*! Assignment operator. */
QXmppJingleIq &QXmppJingleIq::operator=(const QXmppJingleIq &) = default;
/*! Move-assignment operator. */
QXmppJingleIq &QXmppJingleIq::operator=(QXmppJingleIq &&) = default;

/*! Returns the Jingle IQ's action. */
QXmppJingleIq::Action QXmppJingleIq::action() const
{
    return d->action;
}

/*!
    Sets the Jingle IQ's \a action.
*/

void QXmppJingleIq::setAction(QXmppJingleIq::Action action)
{
    d->action = action;
}

/*!
    Adds an element to the IQ's content elements.

    \since QXmpp 0.9.2

    \a content.
*/
void QXmppJingleIq::addContent(const QXmppJingleIq::Content &content)
{
    d->contents << content;
}

/*!
    Returns the IQ's content elements.

    \since QXmpp 0.9.2
*/
QList<QXmppJingleIq::Content> QXmppJingleIq::contents() const
{
    return d->contents;
}

/*!
    Sets the IQ's content elements.

    \since QXmpp 0.9.2

    \a contents.
*/
void QXmppJingleIq::setContents(const QList<QXmppJingleIq::Content> &contents)
{
    d->contents = contents;
}

/*! Returns the session initiator. */
QString QXmppJingleIq::initiator() const
{
    return d->initiator;
}

/*! Sets the session \a initiator. */
void QXmppJingleIq::setInitiator(const QString &initiator)
{
    d->initiator = initiator;
}

/*!
    Returns the reason for the action (in most cases the error).

    \since QXmpp 1.11
*/
std::optional<QXmppJingleReason> QXmppJingleIq::actionReason() const
{
    if (d->actionReasonSet) {
        return d->actionReason;
    }
    return {};
}

/*!
    Sets the reason for the action (in most cases the error).

    \since QXmpp 1.11

    \a reason.
*/
void QXmppJingleIq::setActionReason(const std::optional<QXmppJingleReason> &reason)
{
    d->actionReasonSet = reason.has_value();
    if (reason) {
        d->actionReason = *reason;
    }
}

/*!
    Returns a reference to the IQ's reason element.

    \deprecated Deprecated since QXmpp 1.11: Use actionReason() instead.
*/
QXmppJingleReason &QXmppJingleIq::reason()
{
    return d->actionReason;
}

/*!
    Returns a const reference to the IQ's reason element.

    \deprecated Deprecated since QXmpp 1.11: Use actionReason() instead.
*/
const QXmppJingleReason &QXmppJingleIq::reason() const
{
    return d->actionReason;
}

/*! Returns the session responder. */
QString QXmppJingleIq::responder() const
{
    return d->responder;
}

/*! Sets the session \a responder. */
void QXmppJingleIq::setResponder(const QString &responder)
{
    d->responder = responder;
}

/*!
    Returns true if the call is ringing.

    \deprecated This method is deprecated since QXmpp 1.5. Use \c QXmppJingleIq::rtpSessionState()
    instead.
*/
bool QXmppJingleIq::ringing() const
{
    if (d->rtpSessionState) {
        return std::holds_alternative<RtpSessionStateRinging>(*d->rtpSessionState);
    }

    return false;
}

/*!
    Sets via \a ringing whether the call is ringing.

    \deprecated This method is deprecated since QXmpp 1.5. Use
    \c QXmppJingleIq::setRtpSessionState() instead.
*/
void QXmppJingleIq::setRinging(bool ringing)
{
    if (ringing) {
        d->rtpSessionState = RtpSessionStateRinging();
    } else {
        d->rtpSessionState = std::nullopt;
    }
}

/*! Returns the session ID. */
QString QXmppJingleIq::sid() const
{
    return d->sid;
}

/*!
    Sets the session ID.

    \a sid.
*/
void QXmppJingleIq::setSid(const QString &sid)
{
    d->sid = sid;
}

/*!
    Returns the JID of the \xep{0272}{Multiparty Jingle (Muji)} group chat.

    \since QXmpp 1.5
*/
QString QXmppJingleIq::mujiGroupChatJid() const
{
    return d->mujiGroupChatJid;
}

/*!
    Sets the \a mujiGroupChatJid of the \xep{0272}{Multiparty Jingle (Muji)}
    group chat.

    \since QXmpp 1.5
*/
void QXmppJingleIq::setMujiGroupChatJid(const QString &mujiGroupChatJid)
{
    d->mujiGroupChatJid = mujiGroupChatJid;
}

/*!
    Returns the state of an RTP session as specified by \xep{0167}{Jingle RTP Sessions}
    Informational Messages.

    \since QXmpp 1.5
*/
std::optional<QXmppJingleIq::RtpSessionState> QXmppJingleIq::rtpSessionState() const
{
    return d->rtpSessionState;
}

/*!
    Sets the \a rtpSessionState of an RTP session as specified by \xepn{0167}
    (Jingle RTP Sessions) Informational Messages.

    The appropriate action is set as well.
    Thus, it is not needed to set it manually.

    \since QXmpp 1.5
*/
void QXmppJingleIq::setRtpSessionState(const std::optional<RtpSessionState> &rtpSessionState)
{
    d->rtpSessionState = rtpSessionState;
    d->action = Action::SessionInfo;
}

void QXmppJingleIq::parseElementFromChild(const QDomElement &element)
{
    QDomElement jingleElement = element.firstChildElement(u"jingle"_s);
    d->action = Enums::fromString<Action>(jingleElement.attribute(u"action"_s)).value_or(ContentAccept);
    d->initiator = jingleElement.attribute(u"initiator"_s);
    d->responder = jingleElement.attribute(u"responder"_s);
    d->sid = jingleElement.attribute(u"sid"_s);

    // XEP-0272: Multiparty Jingle (Muji)
    if (auto mujiElement = firstChildElement(jingleElement, u"muji", ns_muji); !mujiElement.isNull()) {
        d->mujiGroupChatJid = mujiElement.attribute(u"room"_s);
    }

    d->contents = parseChildElements<QList<Content>>(jingleElement);

    // reason
    setActionReason(parseOptionalChildElement<QXmppJingleReason>(jingleElement));

    for (const auto &childElement : iterChildElements(jingleElement, {}, ns_jingle_rtp_info)) {
        const auto elementTag = childElement.tagName();

        if (elementTag == u"active") {
            d->rtpSessionState = RtpSessionStateActive();
        } else if (elementTag == u"hold") {
            d->rtpSessionState = RtpSessionStateHold();
        } else if (elementTag == u"unhold") {
            d->rtpSessionState = RtpSessionStateUnhold();
        } else if (elementTag == u"mute" || elementTag == u"unmute") {
            auto creator = childElement.attribute(u"creator"_s);
            if (creator == u"initiator" || creator == u"responder") {
                d->rtpSessionState = RtpSessionStateMuting {
                    elementTag == u"mute",
                    creator == u"initiator" ? Initiator : Responder,
                    childElement.attribute(u"name"_s),
                };
            }
            // otherwise skip element (creator is mandatory)
        } else if (elementTag == u"ringing") {
            d->rtpSessionState = RtpSessionStateRinging();
        }
    }
}

void QXmppJingleIq::toXmlElementFromChild(QXmlStreamWriter *writer) const
{
    XmlWriter w(writer);
    w.write(Element {
        { u"jingle", ns_jingle },
        Attribute { u"action", d->action },
        OptionalAttribute { u"initiator", d->initiator },
        OptionalAttribute { u"responder", d->responder },
        Attribute { u"sid", d->sid },
        // XEP-0272: Multiparty Jingle (Muji)
        OptionalContent {
            !d->mujiGroupChatJid.isEmpty(),
            Element {
                { u"muji", ns_muji },
                Attribute { u"room", d->mujiGroupChatJid },
            },
        },
        d->contents,
        actionReason(),
        [&] {
            if (d->rtpSessionState) {
                if (std::holds_alternative<RtpSessionStateActive>(*d->rtpSessionState)) {
                    w.write(Element { { u"active", ns_jingle_rtp_info } });
                } else if (std::holds_alternative<RtpSessionStateHold>(*d->rtpSessionState)) {
                    w.write(Element { { u"hold", ns_jingle_rtp_info } });
                } else if (std::holds_alternative<RtpSessionStateUnhold>(*d->rtpSessionState)) {
                    w.write(Element { { u"unhold", ns_jingle_rtp_info } });
                } else if (auto muting = std::get_if<RtpSessionStateMuting>(&(*d->rtpSessionState))) {
                    w.write(Element {
                        { muting->isMute ? u"mute" : u"unmute", ns_jingle_rtp_info },
                        Attribute { u"creator", muting->creator == Initiator ? u"initiator" : u"responder" },
                        OptionalAttribute { u"name", muting->name },
                    });
                } else {
                    w.write(Element { { u"ringing", ns_jingle_rtp_info } });
                }
            }
        },
    });
}

class QXmppJingleCandidatePrivate : public QSharedData
{
public:
    QXmppJingleCandidatePrivate();

    int component;
    QString foundation;
    int generation;
    QHostAddress host;
    QString id;
    int network;
    quint16 port;
    QString protocol;
    int priority;
    QXmppJingleCandidate::Type type;
};

QXmppJingleCandidatePrivate::QXmppJingleCandidatePrivate()
    : component(0), generation(0), network(0), port(0), priority(0), type(QXmppJingleCandidate::HostType)
{
}

/*! Constructs an empty candidate. */
QXmppJingleCandidate::QXmppJingleCandidate()
    : d(new QXmppJingleCandidatePrivate())
{
}

/*! Copy-constructor. */
QXmppJingleCandidate::QXmppJingleCandidate(const QXmppJingleCandidate &other) = default;
/*! Move-constructor. */
QXmppJingleCandidate::QXmppJingleCandidate(QXmppJingleCandidate &&) = default;
QXmppJingleCandidate::~QXmppJingleCandidate() = default;
/*! Assignment operator. */
QXmppJingleCandidate &QXmppJingleCandidate::operator=(const QXmppJingleCandidate &other) = default;
/*! Move-assignment operator. */
QXmppJingleCandidate &QXmppJingleCandidate::operator=(QXmppJingleCandidate &&) = default;

/*! Returns the candidate's component ID. */
int QXmppJingleCandidate::component() const
{
    return d->component;
}

/*! Sets the candidates's \a component ID. */
void QXmppJingleCandidate::setComponent(int component)
{
    d->component = component;
}

/*!
    Returns the candidate's foundation.

    \since QXmpp 0.9
*/
QString QXmppJingleCandidate::foundation() const
{
    return d->foundation;
}

/*!
    Sets the candidate's \a foundation.

    \since QXmpp 0.9
*/
void QXmppJingleCandidate::setFoundation(const QString &foundation)
{
    d->foundation = foundation;
}

/*!
    Returns the candidate's generation.

    \since QXmpp 0.9
*/
int QXmppJingleCandidate::generation() const
{
    return d->generation;
}

/*!
    Sets the candidate's \a generation.

    \since QXmpp 0.9
*/
void QXmppJingleCandidate::setGeneration(int generation)
{
    d->generation = generation;
}

/*! Returns the candidate's host address. */

QHostAddress QXmppJingleCandidate::host() const
{
    return d->host;
}

/*! Sets the candidate's \a host address. */
void QXmppJingleCandidate::setHost(const QHostAddress &host)
{
    d->host = host;
}

/*! Returns the candidate's unique identifier. */
QString QXmppJingleCandidate::id() const
{
    return d->id;
}

/*!
    Sets the candidate's unique identifier.

    \a id.
*/
void QXmppJingleCandidate::setId(const QString &id)
{
    d->id = id;
}

/*! Returns the network index (starting at 0) the candidate is on. */
int QXmppJingleCandidate::network() const
{
    return d->network;
}

/*! Sets the \a network index (starting at 0) the candidate is on. */
void QXmppJingleCandidate::setNetwork(int network)
{
    d->network = network;
}

/*! Returns the candidate's port number. */
quint16 QXmppJingleCandidate::port() const
{
    return d->port;
}

/*! Sets the candidate's \a port number. */
void QXmppJingleCandidate::setPort(quint16 port)
{
    d->port = port;
}

/*! Returns the candidate's priority. */
int QXmppJingleCandidate::priority() const
{
    return d->priority;
}

/*! Sets the candidate's \a priority. */
void QXmppJingleCandidate::setPriority(int priority)
{
    d->priority = priority;
}

/*! Returns the candidate's protocol (e.g. "udp"). */
QString QXmppJingleCandidate::protocol() const
{
    return d->protocol;
}

/*! Sets the candidate's \a protocol (e.g. "udp"). */
void QXmppJingleCandidate::setProtocol(const QString &protocol)
{
    d->protocol = protocol;
}

/*! Returns the candidate type (e.g. "host"). */
QXmppJingleCandidate::Type QXmppJingleCandidate::type() const
{
    return d->type;
}

/*! Sets the candidate \a type (e.g. "host"). */
void QXmppJingleCandidate::setType(QXmppJingleCandidate::Type type)
{
    d->type = type;
}

/*! Returns true if the host address or port are empty. */
bool QXmppJingleCandidate::isNull() const
{
    return d->host.isNull() || !d->port;
}

void QXmppJingleCandidate::parse(const QDomElement &element)
{
    d->component = element.attribute(u"component"_s).toInt();
    d->foundation = element.attribute(u"foundation"_s);
    d->generation = element.attribute(u"generation"_s).toInt();
    d->host = QHostAddress(element.attribute(u"ip"_s));
    d->id = element.attribute(u"id"_s);
    d->network = element.attribute(u"network"_s).toInt();
    d->port = element.attribute(u"port"_s).toInt();
    d->priority = element.attribute(u"priority"_s).toInt();
    d->protocol = element.attribute(u"protocol"_s);
    d->type = Enums::fromString<Type>(element.attribute(u"type"_s)).value_or(HostType);
}

void QXmppJingleCandidate::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element {
        u"candidate",
        Attribute { u"component", d->component },
        Attribute { u"foundation", d->foundation },
        Attribute { u"generation", d->generation },
        Attribute { u"id", d->id },
        Attribute { u"ip", d->host.toString() },
        Attribute { u"network", d->network },  // is optional in schema
        Attribute { u"port", d->port },
        Attribute { u"priority", d->priority },
        Attribute { u"protocol", d->protocol },
        Attribute { u"type", d->type },
    });
}

class QXmppJinglePayloadTypePrivate : public QSharedData
{
public:
    QXmppJinglePayloadTypePrivate();

    unsigned char channels;
    unsigned int clockrate;
    unsigned char id;
    unsigned int maxptime;
    QString name;
    QMap<QString, QString> parameters;
    unsigned int ptime;

    // XEP-0293: Jingle RTP Feedback Negotiation
    QList<QXmppJingleRtpFeedbackProperty> rtpFeedbackProperties;
    QList<QXmppJingleRtpFeedbackInterval> rtpFeedbackIntervals;
};

QXmppJinglePayloadTypePrivate::QXmppJinglePayloadTypePrivate()
    : channels(1), clockrate(0), id(0), maxptime(0), ptime(0)
{
}

QXmppJinglePayloadType::QXmppJinglePayloadType()
    : d(new QXmppJinglePayloadTypePrivate())
{
}

/*! Constructs a copy of other. */
QXmppJinglePayloadType::QXmppJinglePayloadType(const QXmppJinglePayloadType &other)
    : d(other.d)
{
}

QXmppJinglePayloadType::~QXmppJinglePayloadType()
{
}

/*! Returns the number of channels (e.g. 1 for mono, 2 for stereo). */
unsigned char QXmppJinglePayloadType::channels() const
{
    return d->channels;
}

/*! Sets the number of \a channels (e.g. 1 for mono, 2 for stereo). */
void QXmppJinglePayloadType::setChannels(unsigned char channels)
{
    d->channels = channels;
}

/*! Returns the clockrate in Hz, i.e. the number of samples per second. */
unsigned int QXmppJinglePayloadType::clockrate() const
{
    return d->clockrate;
}

/*! Sets the \a clockrate in Hz, i.e. the number of samples per second. */
void QXmppJinglePayloadType::setClockrate(unsigned int clockrate)
{
    d->clockrate = clockrate;
}

/*! Returns the payload type identifier. */
unsigned char QXmppJinglePayloadType::id() const
{
    return d->id;
}

/*!
    Sets the payload type identifier.

    \a id.
*/
void QXmppJinglePayloadType::setId(unsigned char id)
{
    Q_ASSERT(id <= 127);
    d->id = id;
}

/*! Returns the maximum packet time in milliseconds. */
unsigned int QXmppJinglePayloadType::maxptime() const
{
    return d->maxptime;
}

/*!
    Sets the maximum packet type in milliseconds.

    \a maxptime.
*/
void QXmppJinglePayloadType::setMaxptime(unsigned int maxptime)
{
    d->maxptime = maxptime;
}

/*! Returns the payload type name. */
QString QXmppJinglePayloadType::name() const
{
    return d->name;
}

/*! Sets the payload type \a name. */
void QXmppJinglePayloadType::setName(const QString &name)
{
    d->name = name;
}

/*! Returns the payload parameters. */
QMap<QString, QString> QXmppJinglePayloadType::parameters() const
{
    return d->parameters;
}

/*! Sets the payload \a parameters. */
void QXmppJinglePayloadType::setParameters(const QMap<QString, QString> &parameters)
{
    d->parameters = parameters;
}

/*! Returns the packet time in milliseconds (20 by default). */
unsigned int QXmppJinglePayloadType::ptime() const
{
    return d->ptime ? d->ptime : 20;
}

/*!
    Sets the packet time in milliseconds (20 by default).

    \a ptime.
*/
void QXmppJinglePayloadType::setPtime(unsigned int ptime)
{
    d->ptime = ptime;
}

/*!
    Returns the RTP feedback properties.

    \since QXmpp 1.5
*/
QList<QXmppJingleRtpFeedbackProperty> QXmppJinglePayloadType::rtpFeedbackProperties() const
{
    return d->rtpFeedbackProperties;
}

/*!
    Sets the \a rtpFeedbackProperties of RTP feedback.

    \since QXmpp 1.5
*/
void QXmppJinglePayloadType::setRtpFeedbackProperties(const QList<QXmppJingleRtpFeedbackProperty> &rtpFeedbackProperties)
{
    d->rtpFeedbackProperties = rtpFeedbackProperties;
}

/*!
    Returns the RTP feedback intervals.
*/
QList<QXmppJingleRtpFeedbackInterval> QXmppJinglePayloadType::rtpFeedbackIntervals() const
{
    return d->rtpFeedbackIntervals;
}

/*!
    Sets the \a rtpFeedbackIntervals of RTP feedback.
*/
void QXmppJinglePayloadType::setRtpFeedbackIntervals(const QList<QXmppJingleRtpFeedbackInterval> &rtpFeedbackIntervals)
{
    d->rtpFeedbackIntervals = rtpFeedbackIntervals;
}

void QXmppJinglePayloadType::parse(const QDomElement &element)
{
    d->id = parseInt<uint8_t>(element.attribute(u"id"_s)).value_or(0);
    d->name = element.attribute(u"name"_s);
    d->channels = parseInt<uint8_t>(element.attribute(u"channels"_s)).value_or(1);
    d->clockrate = element.attribute(u"clockrate"_s).toUInt();
    d->maxptime = element.attribute(u"maxptime"_s).toUInt();
    d->ptime = element.attribute(u"ptime"_s).toUInt();

    for (const auto &child : iterChildElements(element, u"parameter")) {
        d->parameters.insert(child.attribute(u"name"_s), child.attribute(u"value"_s));
    }

    d->rtpFeedbackProperties = parseChildElements<QList<QXmppJingleRtpFeedbackProperty>>(element);
    d->rtpFeedbackIntervals = parseChildElements<QList<QXmppJingleRtpFeedbackInterval>>(element);
}

void QXmppJinglePayloadType::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter w(writer);
    w.write(Element {
        u"payload-type",
        Attribute { u"id", d->id },
        OptionalAttribute { u"name", d->name },
        OptionalAttribute { u"channels", d->channels > 1 ? std::make_optional(d->channels) : std::nullopt },
        OptionalAttribute { u"clockrate", d->clockrate > 0 ? std::make_optional(d->clockrate) : std::nullopt },
        OptionalAttribute { u"maxptime", d->maxptime > 0 ? std::make_optional(d->maxptime) : std::nullopt },
        OptionalAttribute { u"ptime", d->ptime > 0 ? std::make_optional(d->ptime) : std::nullopt },
        [&] {
            for (auto itr = d->parameters.begin(); itr != d->parameters.end(); itr++) {
                w.write(Element {
                    u"parameter",
                    Attribute { u"name", itr.key() },
                    Attribute { u"value", itr.value() },
                });
            }
        },
        d->rtpFeedbackProperties,
        d->rtpFeedbackIntervals,
    });
}

/*!
    Assigns \a other payload type to this one.
*/

QXmppJinglePayloadType &QXmppJinglePayloadType::operator=(const QXmppJinglePayloadType &other)
{
    d = other.d;
    return *this;
}

/*!
    Returns true if this QXmppJinglePayloadType and \a other refer to the same payload type.
*/

bool QXmppJinglePayloadType::operator==(const QXmppJinglePayloadType &other) const
{
    // FIXME : what to do with m_ptime and m_maxptime?
    if (d->id <= 95) {
        return other.d->id == d->id && other.d->clockrate == d->clockrate;
    } else {
        return other.d->channels == d->channels &&
            other.d->clockrate == d->clockrate &&
            other.d->name.toLower() == d->name.toLower();
    }
}

class QXmppJingleRtpDescriptionPrivate : public QSharedData
{
public:
    QXmppJingleRtpDescriptionPrivate() = default;

    QString media;
    quint32 ssrc = 0;
    QList<QXmppJinglePayloadType> payloadTypes;
};

/*!
    \class QXmppJingleRtpDescription
    \inmodule QXmpp

    \brief Represents a description for \xep{0167}{Jingle RTP Sessions}, including media type,
    streaming source, namespace and payload types.

    \since QXmpp 1.6
*/

QXmppJingleRtpDescription::QXmppJingleRtpDescription()
    : d(new QXmppJingleRtpDescriptionPrivate())
{
}

/*!
    Default constructor

    \since QXmpp 1.14
*/
QXmppJingleRtpDescription::QXmppJingleRtpDescription(const QString &media, quint32 ssrc, const QList<QXmppJinglePayloadType> &payloadTypes)
    : d(new QXmppJingleRtpDescriptionPrivate())
{
    d->media = media;
    d->ssrc = ssrc;
    d->payloadTypes = payloadTypes;
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppJingleRtpDescription)

/*! Returns the media type. */
QString QXmppJingleRtpDescription::media() const
{
    return d->media;
}

/*! Sets the \a media type. */
void QXmppJingleRtpDescription::setMedia(const QString &media)
{
    d->media = media;
}

/*! Returns the streaming source. */
quint32 QXmppJingleRtpDescription::ssrc() const
{
    return d->ssrc;
}

/*!
    Sets the streaming source.

    \a ssrc.
*/
void QXmppJingleRtpDescription::setSsrc(quint32 ssrc)
{
    d->ssrc = ssrc;
}

/*! Adds a \a payload type to the list of payload types. */
void QXmppJingleRtpDescription::addPayloadType(const QXmppJinglePayloadType &payload)
{
    d->payloadTypes.append(payload);
}

/*! Returns a list of payload types. */
const QList<QXmppJinglePayloadType> &QXmppJingleRtpDescription::payloadTypes() const
{
    return d->payloadTypes;
}

/*!
    Sets the list of payload types.

    \a payloadTypes.
*/
void QXmppJingleRtpDescription::setPayloadTypes(const QList<QXmppJinglePayloadType> &payloadTypes)
{
    d->payloadTypes = payloadTypes;
}

void QXmppJingleRtpDescription::parse(const QDomElement &element)
{
    d->media = element.attribute(u"media"_s);
    d->ssrc = parseInt<uint32_t>(element.attribute(u"ssrc"_s)).value_or(0);
    d->payloadTypes = parseChildElements<QList<QXmppJinglePayloadType>>(element);
}

void QXmppJingleRtpDescription::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element {
        { u"description", ns_jingle_rtp },
        OptionalAttribute { u"media", d->media },
        OptionalAttribute { u"ssrc", d->ssrc ? std::make_optional(d->ssrc) : std::nullopt },
        d->payloadTypes,
    });
}

class QXmppSdpParameterPrivate : public QSharedData
{
public:
    QString name;
    QString value;
};

/*!
    \class QXmppSdpParameter
    \inmodule QXmpp

    \brief The QXmppSdpParameter class represents a Session Description Protocol (SDP) parameter
    specified by RFC 4566 and used by several XEPs based on \xep{0166}{Jingle}.

    \since QXmpp 1.5
*/

/*! Constructs a Session Description Protocol parameter. */
QXmppSdpParameter::QXmppSdpParameter()
    : d(new QXmppSdpParameterPrivate())
{
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppSdpParameter)

/*!
    Returns the name of the parameter.
*/
QString QXmppSdpParameter::name() const
{
    return d->name;
}

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

/*!
    Returns the value of the parameter.
*/
QString QXmppSdpParameter::value() const
{
    return d->value;
}

/*!
    Sets the \a value of the parameter.

    A parameter in the form "a=b" can be created by this method.
    Any other form of parameters can be created by not using this method.
    The value stays a default-constructed QString then.
*/
void QXmppSdpParameter::setValue(const QString &value)
{
    d->value = value;
}

void QXmppSdpParameter::parse(const QDomElement &element)
{
    d->name = element.attribute(u"name"_s);
    d->value = element.attribute(u"value"_s);
}

void QXmppSdpParameter::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element {
        u"parameter",
        Attribute { u"name", d->name },
        OptionalAttribute { u"value", d->value },
    });
}

/*!
    Determines whether the given DOM \a element is a Session Description
    Protocol parameter element.

    Returns true if \a element is a Session Description Protocol parameter
    element.
*/
bool QXmppSdpParameter::isSdpParameter(const QDomElement &element)
{
    return element.tagName() == u"parameter";
}

class QXmppJingleRtpCryptoElementPrivate : public QSharedData
{
public:
    uint32_t tag = 0;
    QString cryptoSuite;
    QString keyParams;
    QString sessionParams;
};

/*!
    \class QXmppJingleRtpCryptoElement
    \inmodule QXmpp

    \brief The QXmppJingleRtpCryptoElement class represents the \xep{0167}{Jingle RTP Sessions}
    "crypto" element used for SRTP negotiation.

    \since QXmpp 1.5
*/

/*! Constructs a Jingle RTP crypto element. */
QXmppJingleRtpCryptoElement::QXmppJingleRtpCryptoElement()
    : d(new QXmppJingleRtpCryptoElementPrivate())
{
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppJingleRtpCryptoElement)

/*!
    Returns the tag used as an identifier for the crypto element.
*/
uint32_t QXmppJingleRtpCryptoElement::tag() const
{
    return d->tag;
}

/*!
    Sets the \a tag used as an identifier for the crypto element.
*/
void QXmppJingleRtpCryptoElement::setTag(uint32_t tag)
{
    d->tag = tag;
}

/*!
    Returns the crypto suite used as an identifier for describing the encryption and authentication
    algorithms.
*/
QString QXmppJingleRtpCryptoElement::cryptoSuite() const
{
    return d->cryptoSuite;
}

/*!
    Sets the \a cryptoSuite used as an identifier for describing the encryption
    and authentication algorithms.
*/
void QXmppJingleRtpCryptoElement::setCryptoSuite(const QString &cryptoSuite)
{
    d->cryptoSuite = cryptoSuite;
}

/*!
    Returns the key parameters providing one or more sets of keying material
    for the crypto suite.
*/
QString QXmppJingleRtpCryptoElement::keyParams() const
{
    return d->keyParams;
}

/*!
    Sets the \a keyParams providing one or more sets of keying material for the
    crypto suite.
*/
void QXmppJingleRtpCryptoElement::setKeyParams(const QString &keyParams)
{
    d->keyParams = keyParams;
}

/*!
    Returns the session parameters providing transport-specific data.
*/
QString QXmppJingleRtpCryptoElement::sessionParams() const
{
    return d->sessionParams;
}

/*!
    Sets the \a sessionParams providing transport-specific data.
*/
void QXmppJingleRtpCryptoElement::setSessionParams(const QString &sessionParams)
{
    d->sessionParams = sessionParams;
}

void QXmppJingleRtpCryptoElement::parse(const QDomElement &element)
{
    d->tag = element.attribute(u"tag"_s).toUInt();
    d->cryptoSuite = element.attribute(u"crypto-suite"_s);
    d->keyParams = element.attribute(u"key-params"_s);
    d->sessionParams = element.attribute(u"session-params"_s);
}

void QXmppJingleRtpCryptoElement::toXml(QXmlStreamWriter *writer) const
{
    if (!d->cryptoSuite.isEmpty() && !d->keyParams.isEmpty()) {
        XmlWriter(writer).write(Element {
            u"crypto",
            Attribute { u"tag", d->tag },
            Attribute { u"crypto-suite", d->cryptoSuite },
            Attribute { u"key-params", d->keyParams },
            OptionalAttribute { u"session-params", d->sessionParams },
        });
    }
}

/*!
    Determines whether the given DOM \a element is an RTP crypto element.

    Returns true if \a element is an RTP crypto element.
*/
bool QXmppJingleRtpCryptoElement::isJingleRtpCryptoElement(const QDomElement &element)
{
    return element.tagName() == u"crypto";
}

class QXmppJingleRtpEncryptionPrivate : public QSharedData
{
public:
    bool isRequired = false;
    QList<QXmppJingleRtpCryptoElement> cryptoElements;
};

/*!
    \class QXmppJingleRtpEncryption
    \inmodule QXmpp

    \brief The QXmppJingleRtpEncryption class represents the \xep{0167}{Jingle RTP Sessions}
    "encryption" element used for SRTP negotiation.

    \since QXmpp 1.5
*/

/*! Constructs a Jingle RTP encryption. */
QXmppJingleRtpEncryption::QXmppJingleRtpEncryption()
    : d(new QXmppJingleRtpEncryptionPrivate())
{
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppJingleRtpEncryption)

/*!
    Returns whether encryption via SRTP is required.
*/
bool QXmppJingleRtpEncryption::isRequired() const
{
    return d->isRequired;
}

/*!
    Sets via \a isRequired whether encryption via SRTP is required.
*/
void QXmppJingleRtpEncryption::setRequired(bool isRequired)
{
    d->isRequired = isRequired;
}

/*!
    Returns the crypto elements used for encryption via SRTP.
*/
QList<QXmppJingleRtpCryptoElement> QXmppJingleRtpEncryption::cryptoElements() const
{
    return d->cryptoElements;
}

/*!
    Sets the \a cryptoElements used for encryption via SRTP.
*/
void QXmppJingleRtpEncryption::setCryptoElements(const QList<QXmppJingleRtpCryptoElement> &cryptoElements)
{
    d->cryptoElements = cryptoElements;
}

void QXmppJingleRtpEncryption::parse(const QDomElement &element)
{
    d->isRequired = parseBoolean(element.attribute(u"required"_s)).value_or(false);
    d->cryptoElements = parseChildElements<QList<QXmppJingleRtpCryptoElement>>(element);
}

void QXmppJingleRtpEncryption::toXml(QXmlStreamWriter *writer) const
{
    if (!d->cryptoElements.isEmpty()) {
        XmlWriter(writer).write(Element {
            XmlTag,
            OptionalAttribute { u"required", DefaultedBool { d->isRequired, false } },
            d->cryptoElements,
        });
    }
}

/*!
    Determines whether the given DOM \a element is an RTP encryption element.

    Returns true if \a element is an RTP encryption element.
*/
bool QXmppJingleRtpEncryption::isJingleRtpEncryption(const QDomElement &element)
{
    return elementXmlTag(element) == XmlTag;
}

class QXmppJingleRtpFeedbackPropertyPrivate : public QSharedData
{
public:
    QString type;
    QString subtype;
    QList<QXmppSdpParameter> parameters;
};

/*!
    \class QXmppJingleRtpFeedbackProperty
    \inmodule QXmpp

    \brief The QXmppJingleRtpFeedbackProperty class represents the
    \xep{0293}{Jingle RTP Feedback Negotiation} "rtcp-fb" element.

    \since QXmpp 1.5
*/

/*! Default constructor */
QXmppJingleRtpFeedbackProperty::QXmppJingleRtpFeedbackProperty()
    : d(new QXmppJingleRtpFeedbackPropertyPrivate())
{
}

/*!
    Default constructor

    \since QXmpp 1.14
*/
QXmppJingleRtpFeedbackProperty::QXmppJingleRtpFeedbackProperty(const QString &type, const QString &subtype)
    : d(new QXmppJingleRtpFeedbackPropertyPrivate())
{
    d->type = type;
    d->subtype = subtype;
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppJingleRtpFeedbackProperty)

/*!
    Returns the type of RTP feedback.
*/
QString QXmppJingleRtpFeedbackProperty::type() const
{
    return d->type;
}

/*!
    Sets the \a type of RTP feedback.
*/
void QXmppJingleRtpFeedbackProperty::setType(const QString &type)
{
    d->type = type;
}

/*!
    Returns the subtype for RTP feedback.
*/
QString QXmppJingleRtpFeedbackProperty::subtype() const
{
    return d->subtype;
}

/*!
    Sets the \a subtype of RTP feedback.

    If there is more than one parameter, use QXmppJingleRtpFeedbackProperty::setParameters()
    instead of this method.
*/
void QXmppJingleRtpFeedbackProperty::setSubtype(const QString &subtype)
{
    d->subtype = subtype;
}

/*!
    Returns the parameters of RTP feedback.
*/
QList<QXmppSdpParameter> QXmppJingleRtpFeedbackProperty::parameters() const
{
    return d->parameters;
}

/*!
    Sets the \a parameters of RTP feedback.

    Additional parameters can be set by this method.
    If there is only one parameter, use QXmppJingleRtpFeedbackProperty::setSubtype()
    instead of this method.
*/
void QXmppJingleRtpFeedbackProperty::setParameters(const QList<QXmppSdpParameter> &parameters)
{
    d->parameters = parameters;
}

void QXmppJingleRtpFeedbackProperty::parse(const QDomElement &element)
{
    d->type = element.attribute(u"type"_s);
    d->subtype = element.attribute(u"subtype"_s);
    d->parameters = parseChildElements<QList<QXmppSdpParameter>>(element, u"parameter", ns_jingle_rtcp_fb);
}

void QXmppJingleRtpFeedbackProperty::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element {
        XmlTag,
        Attribute { u"type", d->type },
        // If there are parameters, they must be used instead of the subtype.
        OptionalContent { d->subtype.isEmpty(), d->parameters },
        OptionalAttribute { u"subtype", d->subtype },
    });
}

/*!
    Determines whether the given DOM \a element is an RTP feedback property element.

    Returns true if \a element is an RTP feedback property element.
*/
bool QXmppJingleRtpFeedbackProperty::isJingleRtpFeedbackProperty(const QDomElement &element)
{
    return elementXmlTag(element) == XmlTag;
}

/*!
    \class QXmppJingleRtpFeedbackInterval
    \inmodule QXmpp

    \brief The QXmppJingleRtpFeedbackInterval class represents the
    \xep{0293}{Jingle RTP Feedback Negotiation} "rtcp-fb-trr-int" element.

    \since QXmpp 1.5
*/

/*! Constructs a Jingle RTP feedback interval. */
QXmppJingleRtpFeedbackInterval::QXmppJingleRtpFeedbackInterval()
{
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppJingleRtpFeedbackInterval)

/*!
    Returns the value of the RTP feedback interval.
*/
uint64_t QXmppJingleRtpFeedbackInterval::value() const
{
    return m_value;
}

/*!
    Sets the \a value of the RTP feedback interval.
*/
void QXmppJingleRtpFeedbackInterval::setValue(uint64_t value)
{
    m_value = value;
}

void QXmppJingleRtpFeedbackInterval::parse(const QDomElement &element)
{
    m_value = element.attribute(u"value"_s).toUInt();
}

void QXmppJingleRtpFeedbackInterval::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element {
        XmlTag,
        Attribute { u"value", m_value },
    });
}

/*!
    Determines whether the given DOM \a element is an RTP feedback interval
    element.

    Returns true if \a element is an RTP feedback interval element.
*/
bool QXmppJingleRtpFeedbackInterval::isJingleRtpFeedbackInterval(const QDomElement &element)
{
    return elementXmlTag(element) == XmlTag;
}

class QXmppJingleRtpHeaderExtensionPropertyPrivate : public QSharedData
{
public:
    uint32_t id = 0;
    QString uri;
    QXmppJingleRtpHeaderExtensionProperty::Senders senders = QXmppJingleRtpHeaderExtensionProperty::Both;
    QList<QXmppSdpParameter> parameters;
};

/*!
    \enum QXmppJingleRtpHeaderExtensionProperty::Senders

    Parties that are allowed to send the negotiated RTP header extension.

    \value Both The initiator and the sender are allowed.
    \value Initiator Only the initiator is allowed.
    \value Responder Only the responder is allowed.
*/

/*!
    \class QXmppJingleRtpHeaderExtensionProperty
    \inmodule QXmpp

    \brief The QXmppJingleRtpHeaderExtensionProperty class represents the
    \xep{0294}{Jingle RTP Header Extensions Negotiation} "rtp-hdrext" element.

    \since QXmpp 1.5
*/

/*! Default constructor */
QXmppJingleRtpHeaderExtensionProperty::QXmppJingleRtpHeaderExtensionProperty()
    : d(new QXmppJingleRtpHeaderExtensionPropertyPrivate())
{
}

/*!
    Default constructor

    \since QXmpp 1.14
*/
QXmppJingleRtpHeaderExtensionProperty::QXmppJingleRtpHeaderExtensionProperty(const QString &uri, uint32_t id)
    : d(new QXmppJingleRtpHeaderExtensionPropertyPrivate())
{
    d->uri = uri;
    d->id = id;
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppJingleRtpHeaderExtensionProperty)

/*!
    Returns the ID of the RTP header extension.

    The ID is 0 if it is unset.
*/
uint32_t QXmppJingleRtpHeaderExtensionProperty::id() const
{
    return d->id;
}

/*!
    Sets the \a id of the RTP header extension.

    The ID must either be at least 1 and at most 256 or at least 4096 and at most 4351.
*/
void QXmppJingleRtpHeaderExtensionProperty::setId(uint32_t id)
{
    d->id = id;
}

/*!
    Returns the URI defning the RTP header extension.
*/
QString QXmppJingleRtpHeaderExtensionProperty::uri() const
{
    return d->uri;
}

/*!
    Sets the \a uri defning the RTP header extension.
*/
void QXmppJingleRtpHeaderExtensionProperty::setUri(const QString &uri)
{
    d->uri = uri;
}

/*!
    Returns the parties that are allowed to send the negotiated RTP header extensions.
*/
QXmppJingleRtpHeaderExtensionProperty::Senders QXmppJingleRtpHeaderExtensionProperty::senders() const
{
    return d->senders;
}

/*!
    Sets the \a senders that are allowed to send the negotiated RTP header
    extensions.
*/
void QXmppJingleRtpHeaderExtensionProperty::setSenders(Senders senders)
{
    d->senders = senders;
}

/*!
    Returns the parameters of the RTP header extension.
*/
QList<QXmppSdpParameter> QXmppJingleRtpHeaderExtensionProperty::parameters() const
{
    return d->parameters;
}

/*!
    Sets the \a parameters of the RTP header extension.

    Additional parameters can be set by this method.
*/
void QXmppJingleRtpHeaderExtensionProperty::setParameters(const QList<QXmppSdpParameter> &parameters)
{
    d->parameters = parameters;
}

void QXmppJingleRtpHeaderExtensionProperty::parse(const QDomElement &element)
{
    if (elementXmlTag(element) == XmlTag) {
        d->id = element.attribute(u"id"_s).toUInt();
        d->uri = element.attribute(u"uri"_s);
        d->senders = Enums::fromString<Senders>(element.attribute(u"senders"_s))
                         .value_or(Both);
        d->parameters = parseChildElements<QList<QXmppSdpParameter>>(element, u"parameter", ns_jingle_rtp_hdrext);
    }
}

void QXmppJingleRtpHeaderExtensionProperty::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element {
        XmlTag,
        Attribute { u"id", d->id },
        Attribute { u"uri", d->uri },
        OptionalContent { d->senders != Both, Attribute { u"senders", d->senders } },
        d->parameters,
    });
}

/*!
    Determines whether the given DOM \a element is an RTP header extensions
    property element.

    Returns true if \a element is an RTP header extension property element.
*/
bool QXmppJingleRtpHeaderExtensionProperty::isJingleRtpHeaderExtensionProperty(const QDomElement &element)
{
    return elementXmlTag(element) == XmlTag;
}

class QXmppJingleMessageInitiationElementPrivate : public QSharedData
{
public:
    QXmppJingleMessageInitiationElement::Type type = QXmppJingleMessageInitiationElement::Type::None;
    QString id;

    QList<QXmppJingleRtpDescription> descriptions;
    std::optional<QXmppJingleReason> reason;
    QString migratedTo;

    bool containsTieBreak = false;
};

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppJingleReason)

/*!
    \enum QXmppJingleMessageInitiationElement::Type

    Possible types of Jingle Message Initiation elements.

    \value None No type set.
    \value Propose Proposes a new Jingle session.
    \value Ringing Indicates that the callee is alerted.
    \value Proceed Indicates that the callee accepts the session proposal.
    \value Reject Indicates that the callee rejects the session proposal.
    \value Retract Retracts a previously sent session proposal.
    \value Finish Indicates that the session has ended.
*/

/*!
    \class QXmppJingleMessageInitiationElement
    \inmodule QXmpp

    \brief The QXmppJingleMessageInitiationElement class represents a Jingle Message Initiation
    element as specified by \xep{0353}{Jingle Message Initiation}.

    \ingroup Stanzas

    \since QXmpp 1.6
*/

/*! \brief Constructs a Jingle Message Initiation element. */
QXmppJingleMessageInitiationElement::QXmppJingleMessageInitiationElement()
    : d(new QXmppJingleMessageInitiationElementPrivate())
{
}

/*! Returns the Jingle Message Initiation element type */
QXmppJingleMessageInitiationElement::Type QXmppJingleMessageInitiationElement::type() const
{
    return d->type;
}

/*! Sets the Jingle Message Initiation element \a type. */
void QXmppJingleMessageInitiationElement::setType(Type type)
{
    d->type = type;
}

/*! Returns the Jingle Message Initiation element id. */
QString QXmppJingleMessageInitiationElement::id() const
{
    return d->id;
}

/*! Sets the Jingle Message Initiation element \a id. */
void QXmppJingleMessageInitiationElement::setId(const QString &id)
{
    d->id = id;
}

/*! Returns the Jingle Message Initiation element descriptions. */
std::optional<QXmppJingleRtpDescription> QXmppJingleMessageInitiationElement::description() const
{
    if (d->descriptions.isEmpty()) {
        return {};
    }
    return d->descriptions.first();
}

/*!
    Sets the Jingle Message Initiation element descriptions.

    \a description.
*/
void QXmppJingleMessageInitiationElement::setDescription(std::optional<QXmppJingleRtpDescription> description)
{
    if (description.has_value()) {
        d->descriptions = { description.value() };
    } else {
        d->descriptions.clear();
    }
}

/*!
    Returns the Jingle Message Initiation element descriptions.

    \since QXmpp 1.14
*/
QList<QXmppJingleRtpDescription> QXmppJingleMessageInitiationElement::descriptions() const
{
    return d->descriptions;
}

/*!
    Sets the Jingle Message Initiation element descriptions.

    \since QXmpp 1.14

    \a descriptions.
*/
void QXmppJingleMessageInitiationElement::setDescriptions(const QList<QXmppJingleRtpDescription> &descriptions)
{
    d->descriptions = descriptions;
}

/*! Returns the Jingle Message Initiation element reason. */
std::optional<QXmppJingleReason> QXmppJingleMessageInitiationElement::reason() const
{
    return d->reason;
}

/*! Sets the Jingle Message Initiation element \a reason. */
void QXmppJingleMessageInitiationElement::setReason(std::optional<QXmppJingleReason> reason)
{
    d->reason = reason;
}

/*! Returns true if the Jingle Message Initiation element contains a <tie-break/> tag. */
bool QXmppJingleMessageInitiationElement::containsTieBreak() const
{
    return d->containsTieBreak;
}

/*!
    Sets if the Jingle Message Initiation element contains a <tie-break/> tag.

    \a containsTieBreak.
*/
void QXmppJingleMessageInitiationElement::setContainsTieBreak(bool containsTieBreak)
{
    d->containsTieBreak = containsTieBreak;
}

/*!
    Returns the Jingle Message Initiation element ID migrated to if the Jingle is being migrated
    to a different device.
*/
QString QXmppJingleMessageInitiationElement::migratedTo() const
{
    return d->migratedTo;
}

/*!
    Sets the Jingle Message Initiation element ID migrated to if the Jingle is being migrated
    to a different device.

    \a migratedTo.
*/
void QXmppJingleMessageInitiationElement::setMigratedTo(const QString &migratedTo)
{
    d->migratedTo = migratedTo;
}

void QXmppJingleMessageInitiationElement::parse(const QDomElement &element)
{
    if (auto type = Enums::fromString<Type>(element.tagName())) {
        d->type = type.value();
    } else {
        return;
    }
    d->id = element.attribute(u"id"_s);

    // Type::Proceed and Type::Ringing don't need any parsing aside of the id.
    switch (d->type) {
    case Type::Propose:
        d->descriptions = parseChildElements<QList<QXmppJingleRtpDescription>>(element);
        break;
    case Type::Reject:
    case Type::Retract:
        d->containsTieBreak = !firstChildElement(element, u"tie-break").isNull();
        d->reason = parseOptionalChildElement<QXmppJingleReason>(element);

        break;
    case Type::Finish:
        d->reason = parseOptionalChildElement<QXmppJingleReason>(element);
        if (auto migratedToElement = firstChildElement(element, u"migrated"); !migratedToElement.isNull()) {
            d->migratedTo = migratedToElement.attribute(u"to"_s);
        }

        break;
    default:
        break;
    }
}

void QXmppJingleMessageInitiationElement::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element {
        Tag { d->type, ns_jingle_message },
        Attribute { u"id", d->id },
        d->descriptions,
        d->reason,
        OptionalContent { d->containsTieBreak, Element { u"tie-break" } },
        OptionalContent {
            !d->migratedTo.isEmpty(),
            Element {
                u"migrated",
                Attribute { u"to", d->migratedTo },
            },
        },
    });
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppJingleMessageInitiationElement)

/*! Returns true if passed QDomElement is a Jingle Message Initiation \a element */
bool QXmppJingleMessageInitiationElement::isJingleMessageInitiationElement(const QDomElement &element)
{
    return Enums::fromString<Type>(element.tagName()).has_value() &&
        element.hasAttribute(u"id"_s) &&
        element.namespaceURI() == ns_jingle_message;
}

class QXmppCallInviteElementPrivate : public QSharedData
{
public:
    QXmppCallInviteElement::Type type = QXmppCallInviteElement::Type::None;
    QString id;

    std::optional<QXmppCallInviteElement::Jingle> jingle;
    std::optional<QList<QXmppCallInviteElement::External>> external;

    bool audio = true;
    bool video = false;
};

/*!
    \enum QXmppCallInviteElement::Type

    Possible types of Call Invite elements.

    \value None No type set.
    \value Invite Invites a contact to a call.
    \value Retract Retracts a previously sent call invite.
    \value Accept Accepts an incoming call invite.
    \value Reject Rejects an incoming call invite.
    \value Left Indicates that the sender has left the call.
*/

/*!
    \class QXmppCallInviteElement
    \inmodule QXmpp

    \brief The QXmppCallInviteElement class represents a Call Invite
    element as specified by \xep{0482}{Call Invites}.

    \ingroup Stanzas

    \since QXmpp 1.6
*/

/*! \brief Constructs a Call Invite element. */
QXmppCallInviteElement::QXmppCallInviteElement()
    : d(new QXmppCallInviteElementPrivate())
{
}

/*! Returns the Call Invite element type. */
QXmppCallInviteElement::Type QXmppCallInviteElement::type() const
{
    return d->type;
}

/*! Sets the Call Invite element \a type. */
void QXmppCallInviteElement::setType(Type type)
{
    d->type = type;
}

/*! Returns the Call Invite element id. */
QString QXmppCallInviteElement::id() const
{
    return d->id;
}

/*! Sets the Call Invite element \a id. */
void QXmppCallInviteElement::setId(const QString &id)
{
    d->id = id;
}

/*! Returns the Call Invite element audio flag. */
bool QXmppCallInviteElement::audio() const
{
    return d->audio;
}

/*! Sets the Call Invite element \a audio flag. */
void QXmppCallInviteElement::setAudio(bool audio)
{
    d->audio = audio;
}

/*! Returns the Call Invite element video flag. */
bool QXmppCallInviteElement::video() const
{
    return d->video;
}

/*! Sets the Call Invite element \a video flag. */
void QXmppCallInviteElement::setVideo(bool video)
{
    d->video = video;
}

/*! Returns a possible Call Invite element "jingle" sub element. */
std::optional<QXmppCallInviteElement::Jingle> QXmppCallInviteElement::jingle() const
{
    return d->jingle;
}

/*! Sets a possible Call Invite \a jingle sub element. */
void QXmppCallInviteElement::setJingle(std::optional<Jingle> jingle)
{
    d->jingle = jingle;
}

/*! Returns possible Call Invite "external" sub elements. */
std::optional<QList<QXmppCallInviteElement::External>> QXmppCallInviteElement::external() const
{
    return d->external;
}

/*! Sets possible Call Invite \a external sub elements. */
void QXmppCallInviteElement::setExternal(std::optional<QList<External>> external)
{
    d->external = external;
}

void QXmppCallInviteElement::parse(const QDomElement &element)
{
    if (auto type = Enums::fromString<Type>(element.tagName())) {
        d->type = type.value();
    } else {
        return;
    }
    d->id = element.attribute(u"id"_s);

    switch (d->type) {
    case Type::Invite:
        d->audio = element.attribute(u"audio"_s, u"true"_s) == u"true" ? true : false;
        d->video = element.attribute(u"video"_s, u"false"_s) == u"true" ? true : false;
        // fall through
    case Type::Accept: {
        if (auto jingleElement = firstChildElement(element, u"jingle"); !jingleElement.isNull()) {
            d->jingle = Jingle();
            d->jingle->parse(jingleElement);
        }

        d->external = parseChildElements<QList<External>>(element);
        if (d->external->isEmpty()) {
            d->external.reset();
        }
        break;
    }
    case Type::Retract:
    case Type::Reject:
    case Type::Left:
    default:
        break;
    }
}

void QXmppCallInviteElement::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter w(writer);
    w.write(Element {
        Tag { d->type, ns_call_invites },
        OptionalAttribute { u"id", d->id },
        OptionalContent {
            d->type == Type::Invite,
            OptionalAttribute { u"audio", DefaultedBool { d->audio, true } },
            OptionalAttribute { u"video", DefaultedBool { d->video, false } },
        },
        OptionalContent {
            d->type == Type::Invite || d->type == Type::Accept,
            d->jingle,
            d->external,
        },
    });
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppCallInviteElement)

/*! Returns true if passed QDomElement is a Call Invite \a element */
bool QXmppCallInviteElement::isCallInviteElement(const QDomElement &element)
{
    auto type = Enums::fromString<Type>(element.tagName());
    return type.has_value() &&
        // "invite" tags don't have an ID yet.
        (element.hasAttribute(u"id"_s) || type == Type::Invite) &&
        element.namespaceURI() == ns_call_invites;
}

void QXmppCallInviteElement::Jingle::parse(const QDomElement &element)
{
    if (element.hasAttribute(u"sid"_s)) {
        sid = element.attribute(u"sid"_s);
    }

    if (element.hasAttribute(u"jid"_s)) {
        jid = element.attribute(u"jid"_s);
    }
}

void QXmppCallInviteElement::Jingle::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element {
        u"jingle",
        Attribute { u"sid", sid },
        OptionalAttribute { u"jid", jid },
    });
}

void QXmppCallInviteElement::External::parse(const QDomElement &el)
{
    uri = el.attribute(u"uri"_s);
}

void QXmppCallInviteElement::External::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter(writer).write(Element { u"external", Attribute { u"uri", uri } });
}
