// SPDX-FileCopyrightText: 2009 Manjeet Dahiya <manjeetdahiya@gmail.com>
// SPDX-FileCopyrightText: 2010 Jeremy Lainé <jeremy.laine@m4x.org>
// SPDX-FileCopyrightText: 2018 Linus Jahn <lnj@kaidan.im>
// SPDX-FileCopyrightText: 2021 Melvin Keskin <melvo@olomono.de>
// SPDX-FileCopyrightText: 2023 Tibor Csötönyi <work@taibsu.de>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppMessage.h"

#include "QXmppBitsOfBinaryDataList.h"
#include "QXmppConstants_p.h"
#include "QXmppDataForm.h"
#include "QXmppFallback.h"
#include "QXmppFileShare.h"
#include "QXmppJingleData.h"
#include "QXmppMessageReaction.h"
#include "QXmppMessage_p.h"
#include "QXmppMixInvitation.h"
#include "QXmppMucForms.h"
#include "QXmppXmlExtensions.h"

#include "Algorithms.h"
#include "StringLiterals.h"
#ifdef BUILD_OMEMO
#include "QXmppOmemoElement_p.h"
#include "QXmppOmemoEnvelope_p.h"
#endif
#include "QXmppGlobal_p.h"
#include "QXmppOutOfBandUrl.h"
#include "QXmppTrustMessageElement.h"
#include "QXmppUtils.h"
#include "QXmppUtils_p.h"
#include "QXmppXmlTags_p.h"

#include "XmlWriter.h"

#include <ranges>

#include <QDateTime>
#include <QDomElement>
#include <QTextStream>
#include <QTimeZone>
#include <QXmlStreamWriter>

using namespace QXmpp;
using namespace QXmpp::Private;
namespace views = std::views;

template<>
struct Enums::Data<QXmppMessage::State> {
    using enum QXmppMessage::State;
    static constexpr auto NullValue = None;
    static constexpr auto Values = makeValues<QXmppMessage::State>({
        { None, {} },
        { Active, u"active" },
        { Inactive, u"inactive" },
        { Gone, u"gone" },
        { Composing, u"composing" },
        { Paused, u"paused" },
    });
};

template<>
struct Enums::Data<QXmppMessage::Marker> {
    using enum QXmppMessage::Marker;
    static constexpr auto Values = makeValues<QXmppMessage::Marker>({
        { NoMarker, {} },
        { Received, u"received" },
        { Displayed, u"displayed" },
        { Acknowledged, u"acknowledged" },
    });
};

template<>
struct Enums::Data<QXmppMessage::Hint> {
    using enum QXmppMessage::Hint;
    static constexpr bool IsFlags = true;
    static constexpr auto Values = makeValues<QXmppMessage::Hint>({
        { NoPermanentStore, u"no-permanent-store" },
        { NoStore, u"no-store" },
        { NoCopy, u"no-copy" },
        { Store, u"store" },
    });
};

constexpr std::array<QStringView, 8> ENCRYPTION_NAMES = {
    QStringView(),
    QStringView(),
    u"OTR",
    u"Legacy OpenPGP",
    u"OpenPGP for XMPP (OX)",
    u"OMEMO",
    u"OMEMO 1",
    u"OMEMO 2",
};

static QStringView encryptionToName(EncryptionMethod encryption)
{
    return ENCRYPTION_NAMES[std::size_t(encryption)];
}

static bool checkElement(const QDomElement &element, QStringView tagName, QStringView xmlns)
{
    return element.tagName() == tagName && element.namespaceURI() == xmlns;
}

/*!
    \struct QXmppStanzaId
    \inmodule QXmpp

    \brief Stanza ID element as defined in \xep{0359}{Unique and Stable Stanza IDs}.

    \since QXmpp 1.8
*/

enum StampType {
    LegacyDelayedDelivery,  // XEP-0091: Legacy Delayed Delivery
    DelayedDelivery         // XEP-0203: Delayed Delivery
};

class QXmppMessagePrivate : public QSharedData
{
public:
    QString body;
    QString e2eeFallbackBody;
    std::optional<QString> subject;
    QString thread;
    QString parentThread;
    QXmppMessage::Type type = QXmppMessage::Chat;

    // XEP-0066: Out of Band Data
    QVector<QXmppOutOfBandUrl> outOfBandUrls;

    // XEP-0071: XHTML-IM
    QString xhtml;

    // XEP-0085: Chat State Notifications
    QXmppMessage::State state = QXmppMessage::None;

    // XEP-0091: Legacy Delayed Delivery | XEP-0203: Delayed Delivery
    QDateTime stamp;
    StampType stampType = DelayedDelivery;

    // XEP-0184: Message Delivery Receipts
    QString receiptId;
    bool receiptRequested = false;

    // XEP-0224: Attention
    bool attentionRequested = false;

    // XEP-0231: Bits of Binary
    QXmppBitsOfBinaryDataList bitsOfBinaryData;

    // XEP-0045: Multi-User Chat
    std::optional<QXmppMucVoiceRequest> mucVoiceRequest;
    std::optional<Muc::UserQuery> mucUserQuery;

    // XEP-0249: Direct MUC Invitations
    std::optional<Muc::DirectInvitation> mucDirectInvitation;

    // XEP-0280: Message Carbons
    bool privatemsg = false;
    bool isCarbonForwarded = false;

    // XEP-0308: Last Message Correction
    QString replaceId;

    // XEP-0333: Chat Markers
    bool markable = false;
    QXmppMessage::Marker marker = QXmppMessage::NoMarker;
    QString markedId;
    QString markedThread;

    // XEP-0334: Message Processing Hints
    QXmppMessage::Hints hints;

    // XEP-0353: Jingle Message Initiation
    std::optional<QXmppJingleMessageInitiationElement> jingleMessageInitiationElement;

    // XEP-0359: Unique and Stable Stanza IDs
    QVector<QXmppStanzaId> stanzaIds;
    QString originId;

    // XEP-0367: Message Attaching
    QString attachId;

    // XEP-0369: Mediated Information eXchange (MIX)
    QString mixUserJid;
    QString mixUserNick;

    // XEP-0380: Explicit Message Encryption
    QString encryptionMethod;
    QString encryptionName;

    // XEP-0382: Spoiler messages
    bool isSpoiler = false;
    QString spoilerHint;
#ifdef BUILD_OMEMO
    // XEP-0384: OMEMO Encryption
    std::optional<QXmppOmemoElement> omemoElement;
#endif

    // XEP-0407: Mediated Information eXchange (MIX): Miscellaneous Capabilities
    std::optional<QXmppMixInvitation> mixInvitation;

    // XEP-0421: Occupant identifiers for semi-anonymous MUCs
    QString mucOccupantId;

    // XEP-0428: Fallback Indication
    QVector<QXmppFallback> fallbackMarkers;

    // XEP-0434: Trust Messages (TM)
    std::optional<QXmppTrustMessageElement> trustMessageElement;

    // XEP-0444: Message Reactions
    std::optional<QXmppMessageReaction> reaction;

    // XEP-0447: Stateless file sharing
    QVector<QXmppFileShare> sharedFiles;
    QVector<QXmppFileSourcesAttachment> fileSourcesAttachments;

    // XEP-0461: Message Replies
    std::optional<Reply> reply;

    // XEP-0482: Call Invites
    std::optional<QXmppCallInviteElement> callInviteElement;

    // Registered and fallback XML extensions
    QXmpp::Xml::Extensions extensions;
};

/*!
    Constructs a QXmppMessage with sender \a from, recipient \a to, message
    \a body and \a thread.
*/
QXmppMessage::QXmppMessage(const QString &from, const QString &to, const QString &body, const QString &thread)
    : QXmppStanza(from, to), d(new QXmppMessagePrivate)
{
    d->body = body;
    d->thread = thread;
}

/*! Constructs a copy of \a other. */
QXmppMessage::QXmppMessage(const QXmppMessage &other) = default;
/*! Move-constructor. */
QXmppMessage::QXmppMessage(QXmppMessage &&) = default;
QXmppMessage::~QXmppMessage() = default;
/*! Assignment operator. */
QXmppMessage &QXmppMessage::operator=(const QXmppMessage &other) = default;
/*! Move-assignment operator. */
QXmppMessage &QXmppMessage::operator=(QXmppMessage &&) = default;

/*!
    Returns whether the QXmppStanza is a stanza in the XMPP sense (i. e. a message,
    iq or presence)

    \since QXmpp 1.0
*/
bool QXmppMessage::isXmppStanza() const
{
    return true;
}

/*! Returns the message's body. */
QString QXmppMessage::body() const
{
    return d->body;
}

/*! Sets the message's \a body. */
void QXmppMessage::setBody(const QString &body)
{
    d->body = body;
}

/*!
    Returns a body that is unlike the normal body not encrypted.

    It can be presented to users if the message could not be decrypted (e.g.,
    because their clients do not support the used end-to-end encryption).

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

/*!
    Sets the end-to-end encryption \a fallbackBody that is unlike the normal
    body not encrypted.

    It can be presented to users if the message could not be decrypted (e.g.,
    because their clients do not support the used end-to-end encryption).

    \since QXmpp 1.5
*/
void QXmppMessage::setE2eeFallbackBody(const QString &fallbackBody)
{
    d->e2eeFallbackBody = fallbackBody;
}

/*! Returns the message's type. */
QXmppMessage::Type QXmppMessage::type() const
{
    return d->type;
}

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

/*!
    Returns the message's subject, or an empty string if not set.

    \note Use hasSubject() to distinguish between a missing and an empty \c <subject/> element.
*/
QString QXmppMessage::subject() const
{
    return d->subject.value_or(QString {});
}

/*!
    Returns whether the message contains a \c <subject/> element.

    \since QXmpp 1.16
*/
bool QXmppMessage::hasSubject() const
{
    return d->subject.has_value();
}

/*!
    Sets or clears the message's \a subject. Pass \c std::nullopt to remove the \c <subject/>
    element.

    \note The setter previously accepted a \c QString. It accepts \c std::optional<QString>
    (enabling \c std::nullopt to remove the element) since QXmpp 1.16.
*/
void QXmppMessage::setSubject(std::optional<QString> subject)
{
    d->subject = std::move(subject);
}

/*! Returns the message's thread. */
QString QXmppMessage::thread() const
{
    return d->thread;
}

/*! Sets the message's \a thread. */
void QXmppMessage::setThread(const QString &thread)
{
    d->thread = thread;
}

/*!
    Returns the optional parent thread of this message.

    The possibility to create child threads was added in RFC6121.

    \since QXmpp 1.3
*/
QString QXmppMessage::parentThread() const
{
    return d->parentThread;
}

/*!
    Sets the optional parent thread of this message.

    The possibility to create child threads was added in RFC6121.

    \since QXmpp 1.3

    \a parent.
*/
void QXmppMessage::setParentThread(const QString &parent)
{
    d->parentThread = parent;
}

/*!
    Returns a possibly attached URL from \xep{0066}{Out of Band Data}

    \since QXmpp 1.0
*/
QString QXmppMessage::outOfBandUrl() const
{
    if (d->outOfBandUrls.empty()) {
        return {};
    }

    return d->outOfBandUrls.front().url();
}

/*!
    Sets the attached URL for \xep{0066}{Out of Band Data}

    This overrides all other urls that may be contained in the list of out of band urls.

    \since QXmpp 1.0

    \a url.
*/
void QXmppMessage::setOutOfBandUrl(const QString &url)
{
    QXmppOutOfBandUrl data;
    data.setUrl(url);
    d->outOfBandUrls = { std::move(data) };
}

/*!
    Returns possibly attached URLs from \xep{0066}{Out of Band Data}

    \since QXmpp 1.5
*/
QVector<QXmppOutOfBandUrl> QXmppMessage::outOfBandUrls() const
{
    return d->outOfBandUrls;
}

/*!
    Sets the attached URLs for \xep{0066}{Out of Band Data}

    \since QXmpp 1.5

    \a urls.
*/
void QXmppMessage::setOutOfBandUrls(const QVector<QXmppOutOfBandUrl> &urls)
{
    d->outOfBandUrls = urls;
}

/*!
    Returns the message's XHTML body as defined by \xep{0071}{XHTML-IM}.

    \since QXmpp 0.6.2
*/
QString QXmppMessage::xhtml() const
{
    return d->xhtml;
}

/*!
    Sets the message's XHTML body as defined by \xep{0071}{XHTML-IM}.

    \since QXmpp 0.6.2

    \a xhtml.
*/
void QXmppMessage::setXhtml(const QString &xhtml)
{
    d->xhtml = xhtml;
}

/*!
    Returns the the chat state notification according to \xep{0085}{Chat State Notifications}.

    \since QXmpp 0.2
*/
QXmppMessage::State QXmppMessage::state() const
{
    return d->state;
}

/*!
    Sets the the chat state notification according to \xep{0085}{Chat State Notifications}.

    \since QXmpp 0.2

    \a state.
*/
void QXmppMessage::setState(QXmppMessage::State state)
{
    d->state = state;
}

/*!
    Returns the optional timestamp of the message specified using \xep{0093}{Legacy Delayed Delivery}
    or using \xep{0203}{Delayed Delivery} (preferred).

    \since QXmpp 0.2
*/
QDateTime QXmppMessage::stamp() const
{
    return d->stamp;
}

/*!
    Sets the message's timestamp without modifying the type of the stamp
    (\xep{0093}{Legacy Delayed Delivery} or \xep{0203}{Delayed Delivery}).

    By default messages are constructed with the new delayed delivery XEP, but
    parsed messages keep their type.

    \since QXmpp 0.2

    \a stamp.
*/
void QXmppMessage::setStamp(const QDateTime &stamp)
{
    d->stamp = stamp;
}

/*!
    Returns true if a delivery receipt is requested, as defined by \xep{0184}{Message Delivery Receipts}.

    \since QXmpp 0.4
*/
bool QXmppMessage::isReceiptRequested() const
{
    return d->receiptRequested;
}

/*!
    Sets whether a delivery receipt is requested, as defined by \xep{0184}{Message Delivery Receipts}.

    \since QXmpp 0.4

    \a requested.
*/
void QXmppMessage::setReceiptRequested(bool requested)
{
    d->receiptRequested = requested;
    if (requested && id().isEmpty()) {
        generateAndSetNextId();
    }
}

/*!
    If this message is a delivery receipt, returns the ID of the original
    message.

    \since QXmpp 0.4
*/
QString QXmppMessage::receiptId() const
{
    return d->receiptId;
}

/*!
    Make this message a delivery receipt for the message with the given \a id.

    \since QXmpp 0.4
*/
void QXmppMessage::setReceiptId(const QString &id)
{
    d->receiptId = id;
}

/*!
    Returns true if the user's attention is requested, as defined by \xep{0224}{Attention}.

    \since QXmpp 0.4
*/
bool QXmppMessage::isAttentionRequested() const
{
    return d->attentionRequested;
}

/*!
    Sets whether the user's attention is \a requested (true) or not (false), as
    defined by \xep{0224}{Attention}.

    \since QXmpp 0.4
*/
void QXmppMessage::setAttentionRequested(bool requested)
{
    d->attentionRequested = requested;
}

/*!
    Returns a list of data packages attached using \xep{0231}{Bits of Binary}.

    This could be used to resolve \c cid: URIs found in the X-HTML body.

    \since QXmpp 1.2
*/
QXmppBitsOfBinaryDataList QXmppMessage::bitsOfBinaryData() const
{
    return d->bitsOfBinaryData;
}

/*!
    Returns a list of data attached using \xep{0231}{Bits of Binary}.

    This could be used to resolve \c cid: URIs found in the X-HTML body.

    \since QXmpp 1.2
*/
QXmppBitsOfBinaryDataList &QXmppMessage::bitsOfBinaryData()
{
    return d->bitsOfBinaryData;
}

/*!
    Sets a list of \xep{0231}{Bits of Binary} attachments to be included.

    \since QXmpp 1.2

    \a bitsOfBinaryData.
*/
void QXmppMessage::setBitsOfBinaryData(const QXmppBitsOfBinaryDataList &bitsOfBinaryData)
{
    d->bitsOfBinaryData = bitsOfBinaryData;
}

/*!
    Returns whether the given text is a '/me command' as defined in \xep{0245}{The /me Command}.

    \since QXmpp 1.3
*/
bool QXmppMessage::isSlashMeCommand(const QString &body)
{
    return body.startsWith(u"/me "_s);
}

/*!
    Returns whether the body of the message is a '/me command' as defined in
    \xep{0245}{The /me Command}.

    \note If you want to check a custom string for the /me command, you can use
    the static version of this method. This can be helpful when checking user
    input before a message was sent.

    \since QXmpp 1.3
*/
bool QXmppMessage::isSlashMeCommand() const
{
    return isSlashMeCommand(d->body);
}

/*!
    Returns the part of the body after the /me command.

    This cuts off '/me ' (with the space) from the body, in case the body
    starts with that. In case the body does not contain a /me command as
    defined in \xep{0245}{The /me Command}, a null string is returned.

    This is useful when displaying the /me command correctly to the user.

    \since QXmpp 1.3
*/
QString QXmppMessage::slashMeCommandText(const QString &body)
{
    if (isSlashMeCommand(body)) {
        return body.mid(4);
    }
    return {};
}

/*!
    Returns the part of the body after the /me command.

    This cuts off '/me ' (with the space) from the body, in case the body
    starts with that. In case the body does not contain a /me command as
    defined in \xep{0245}{The /me Command}, a null string is returned.

    This is useful when displaying the /me command correctly to the user.

    \since QXmpp 1.3
*/
QString QXmppMessage::slashMeCommandText() const
{
    return slashMeCommandText(d->body);
}

/*!
    Returns the MUC status codes from a \xep{0045}{Multi-User Chat} message.

    Status codes are sent by the server to notify about room configuration changes,
    role/affiliation changes, and other events (e.g., code 104 for room config change).

    \since QXmpp 1.16
*/
QList<uint32_t> QXmppMessage::mucStatusCodes() const
{
    return d->mucUserQuery ? d->mucUserQuery->statusCodes() : QList<uint32_t>();
}

/*!
    Sets the MUC status \a codes for a \xep{0045}{Multi-User Chat} message.

    This is primarily useful for constructing test messages or forwarded stanzas.
    For messages received from the server, status codes are parsed automatically.

    \since QXmpp 1.16
*/
void QXmppMessage::setMucStatusCodes(const QList<uint32_t> &codes)
{
    if (!d->mucUserQuery) {
        d->mucUserQuery = QXmpp::Muc::UserQuery();
    }
    d->mucUserQuery->setStatusCodes(codes);
}

/*!
    Returns the \xep{0045}{Multi-User Chat} voice request contained in this message, if any.

    Voice requests use a \c muc#request data form in a normal-type message.
    Moderators receive these when a visitor requests voice in a moderated room.

    \since QXmpp 1.16
*/
std::optional<QXmppMucVoiceRequest> QXmppMessage::mucVoiceRequest() const
{
    return d->mucVoiceRequest;
}

/*!
    Sets the \xep{0045}{Multi-User Chat} voice \a request for this message.

    This is primarily useful for constructing messages in tests or for forwarding stanzas.
    When responding to a received voice request as a moderator, use
    QXmppMucRoomV2::answerVoiceRequest() instead.

    \since QXmpp 1.16
*/
void QXmppMessage::setMucVoiceRequest(std::optional<QXmppMucVoiceRequest> request)
{
    d->mucVoiceRequest = std::move(request);
}

/*!
    Returns the parsed \c x element with namespace \c muc#user, if any.

    This element carries mediated invitations, invitation declines, and status codes.

    \since QXmpp 1.16
*/
std::optional<QXmpp::Muc::UserQuery> QXmppMessage::mucUserQuery() const
{
    return d->mucUserQuery;
}

/*!
    Sets the \c x element with namespace \c muc#user on this \a element.

    \since QXmpp 1.16
*/
void QXmppMessage::setMucUserQuery(std::optional<QXmpp::Muc::UserQuery> element)
{
    d->mucUserQuery = std::move(element);
}

/*!
    Returns the direct MUC invitation (\xep{0249}{Direct MUC Invitations}) if present.

    \since QXmpp 1.16
*/
std::optional<QXmpp::Muc::DirectInvitation> QXmppMessage::mucDirectInvitation() const
{
    return d->mucDirectInvitation;
}

/*!
    Sets the direct MUC \a invitation (\xep{0249}{Direct MUC Invitations}).

    \since QXmpp 1.16
*/
void QXmppMessage::setMucDirectInvitation(std::optional<QXmpp::Muc::DirectInvitation> invitation)
{
    d->mucDirectInvitation = std::move(invitation);
}

/*!
    Returns the JID for a multi-user chat direct invitation as defined by
    \xep{0249}{Direct MUC Invitations}.

    \deprecated Use mucDirectInvitation() instead.
    \since QXmpp 0.7.4
*/
QString QXmppMessage::mucInvitationJid() const
{
    return d->mucDirectInvitation ? d->mucDirectInvitation->jid() : QString();
}

/*!
    Sets the JID for a multi-user chat direct invitation as defined by
    \xep{0249}{Direct MUC Invitations}.

    \deprecated Use setMucDirectInvitation() instead.
    \since QXmpp 0.7.4

    \a jid.
*/
void QXmppMessage::setMucInvitationJid(const QString &jid)
{
    if (!d->mucDirectInvitation) {
        d->mucDirectInvitation.emplace();
    }
    d->mucDirectInvitation->setJid(jid);
}

/*!
    Returns the password for a multi-user chat direct invitation as defined by
    \xep{0249}{Direct MUC Invitations}.

    \deprecated Use mucDirectInvitation() instead.
    \since QXmpp 0.7.4
*/
QString QXmppMessage::mucInvitationPassword() const
{
    return d->mucDirectInvitation ? d->mucDirectInvitation->password() : QString();
}

/*!
    Sets the \a password for a multi-user chat direct invitation as defined by
    \xep{0249}{Direct MUC Invitations}.

    \deprecated Use setMucDirectInvitation() instead.
    \since QXmpp 0.7.4
*/
void QXmppMessage::setMucInvitationPassword(const QString &password)
{
    if (!d->mucDirectInvitation) {
        d->mucDirectInvitation.emplace();
    }
    d->mucDirectInvitation->setPassword(password);
}

/*!
    Returns the reason for a multi-user chat direct invitation as defined by
    \xep{0249}{Direct MUC Invitations}.

    \deprecated Use mucDirectInvitation() instead.
    \since QXmpp 0.7.4
*/
QString QXmppMessage::mucInvitationReason() const
{
    return d->mucDirectInvitation ? d->mucDirectInvitation->reason() : QString();
}

/*!
    Sets the \a reason for a multi-user chat direct invitation as defined by
    \xep{0249}{Direct MUC Invitations}.

    \deprecated Use setMucDirectInvitation() instead.
    \since QXmpp 0.7.4
*/
void QXmppMessage::setMucInvitationReason(const QString &reason)
{
    if (!d->mucDirectInvitation) {
        d->mucDirectInvitation.emplace();
    }
    d->mucDirectInvitation->setReason(reason);
}

/*!
    Returns if the message is marked with a &lt;private/&gt; tag, in which case
    it will not be forwarded to other resources according to \xep{0280}{Message Carbons}.

    \since QXmpp 1.0
*/
bool QXmppMessage::isPrivate() const
{
    return d->privatemsg;
}

/*!
    If true is passed, the message is marked with a &lt;private/&gt; tag, in
    which case it will not be forwarded to other resources according to
    \xep{0280}{Message Carbons}.

    \since QXmpp 1.0

    \a priv.
*/
void QXmppMessage::setPrivate(const bool priv)
{
    d->privatemsg = priv;
}

/*!
    Returns whether this message has been forwarded using carbons.

    \since QXmpp 1.5
*/
bool QXmppMessage::isCarbonForwarded() const
{
    return d->isCarbonForwarded;
}

/*!
    Sets whether this message has been forwarded using carbons.

    Setting this to true has no effect, this is purely informational.

    \since QXmpp 1.5

    \a forwarded.
*/
void QXmppMessage::setCarbonForwarded(bool forwarded)
{
    d->isCarbonForwarded = forwarded;
}

/*!
    Returns the message id to replace with this message as used in \xep{0308}{Last Message Correction}.
    If the returned string is empty, this message is not replacing another.

    \since QXmpp 1.0
*/
QString QXmppMessage::replaceId() const
{
    return d->replaceId;
}

/*!
    Sets the message id to replace with this message as in \xep{0308}{Last Message Correction}.

    \since QXmpp 1.0

    \a replaceId.
*/
void QXmppMessage::setReplaceId(const QString &replaceId)
{
    d->replaceId = replaceId;
}

/*!
    Returns true if a message is markable, as defined by \xep{0333}{Chat Markers}.

    \since QXmpp 0.8.1
*/
bool QXmppMessage::isMarkable() const
{
    return d->markable;
}

/*!
    Sets if the message is markable, as defined by \xep{0333}{Chat Markers}.

    \since QXmpp 0.8.1

    \a markable.
*/
void QXmppMessage::setMarkable(const bool markable)
{
    d->markable = markable;
}

/*!
    Returns the message's marker id, as defined by \xep{0333}{Chat Markers}.

    \since QXmpp 0.8.1
*/
QString QXmppMessage::markedId() const
{
    return d->markedId;
}

/*!
    Sets the message's marker id, as defined by \xep{0333}{Chat Markers}.

    \since QXmpp 0.8.1

    \a markerId.
*/
void QXmppMessage::setMarkerId(const QString &markerId)
{
    d->markedId = markerId;
}

/*!
    Returns the message's marker thread, as defined by \xep{0333}{Chat Markers}.

    \since QXmpp 0.8.1
*/
QString QXmppMessage::markedThread() const
{
    return d->markedThread;
}

/*!
    Sets the message's marked thread, as defined by \xep{0333}{Chat Markers}.

    \since QXmpp 0.8.1

    \a markedThread.
*/
void QXmppMessage::setMarkedThread(const QString &markedThread)
{
    d->markedThread = markedThread;
}

/*!
    Returns the message's marker, as defined by \xep{0333}{Chat Markers}.

    \since QXmpp 0.8.1
*/
QXmppMessage::Marker QXmppMessage::marker() const
{
    return d->marker;
}

/*!
    Sets the message's marker, as defined by \xep{0333}{Chat Markers}

    \since QXmpp 0.8.1

    \a marker.
*/
void QXmppMessage::setMarker(const Marker marker)
{
    d->marker = marker;
}

/*!
    Returns true if the message contains the \a hint passed, as defined in
    \xep{0334}{Message Processing Hints}

    \since QXmpp 1.1
*/
bool QXmppMessage::hasHint(const Hint hint) const
{
    return d->hints & hint;
}

/*!
    Adds a \a hint to the message, as defined in \xep{0334}{Message Processing Hints}

    \since QXmpp 1.1
*/
void QXmppMessage::addHint(const Hint hint)
{
    d->hints |= hint;
}

/*!
    Removes a hint from the message, as defined in \xep{0334}{Message Processing Hints}

    \since QXmpp 1.1

    \a hint.
*/
void QXmppMessage::removeHint(const Hint hint)
{
    d->hints &= ~hint;
}

/*!
    Removes all hints from the message, as defined in \xep{0334}{Message Processing Hints}

    \since QXmpp 1.1
*/
void QXmppMessage::removeAllHints()
{
    d->hints = {};
}

/*! Returns a Jingle Message Initiation element as defined in \xep{0353}{Jingle Message Initiation}. */
std::optional<QXmppJingleMessageInitiationElement> QXmppMessage::jingleMessageInitiationElement() const
{
    return d->jingleMessageInitiationElement;
}

/*!
    Sets a Jingle Message Initiation element as defined in \xep{0353}{Jingle Message Initiation}.

    \a jingleMessageInitiationElement.
*/
void QXmppMessage::setJingleMessageInitiationElement(const std::optional<QXmppJingleMessageInitiationElement> &jingleMessageInitiationElement)
{
    d->jingleMessageInitiationElement = jingleMessageInitiationElement;
}

/*!
    Returns the stanza ID of the message according to \xep{0359}{Unique and Stable Stanza IDs}.

    \deprecated Use stanzaIds() instead.

    \since QXmpp 1.3
*/
QString QXmppMessage::stanzaId() const
{
    return d->stanzaIds.empty() ? QString() : d->stanzaIds.last().id;
}

/*!
    Sets the stanza ID of the message according to \xep{0359}{Unique and Stable Stanza IDs}.

    \deprecated Use setStanzaIds() instead.

    \since QXmpp 1.3
*/
void QXmppMessage::setStanzaId(const QString &id)
{
    if (d->stanzaIds.size() == 1) {
        d->stanzaIds.first().id = id;
    } else {
        d->stanzaIds = { QXmppStanzaId { id, {} } };
    }
}

/*!
    Returns the creator of the stanza ID according to \xep{0359}{Unique and Stable Stanza IDs}.

    \deprecated Use stanzaIds() instead.

    \since QXmpp 1.3
*/
QString QXmppMessage::stanzaIdBy() const
{
    return d->stanzaIds.empty() ? QString() : d->stanzaIds.last().by;
}

/*!
    Sets the creator of the stanza ID according to \xep{0359}{Unique and Stable Stanza IDs}.

    \deprecated Use setStanzaIds() instead.

    \since QXmpp 1.3
*/
void QXmppMessage::setStanzaIdBy(const QString &by)
{
    if (d->stanzaIds.size() == 1) {
        d->stanzaIds.first().by = by;
    } else {
        d->stanzaIds = { QXmppStanzaId { {}, by } };
    }
}

/*!
    Returns the stanza IDs of the message as defined in \xep{0359}{Unique and Stable Stanza IDs}.

    \since QXmpp 1.8
*/
QVector<QXmppStanzaId> QXmppMessage::stanzaIds() const
{
    return d->stanzaIds;
}

/*!
    Sets the stanza IDs of the message as defined in \xep{0359}{Unique and Stable Stanza IDs}.

    \since QXmpp 1.8

    \a ids.
*/
void QXmppMessage::setStanzaIds(const QVector<QXmppStanzaId> &ids)
{
    d->stanzaIds = ids;
}

/*!
    Returns the origin ID of the message according to \xep{0359}{Unique and Stable Stanza IDs}.

    \since QXmpp 1.3
*/
QString QXmppMessage::originId() const
{
    return d->originId;
}

/*!
    Sets the origin ID of the message according to \xep{0359}{Unique and Stable Stanza IDs}.

    \since QXmpp 1.3

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

/*!
    Returns the message id this message is linked/attached to. See \xep{0367}{Message Attaching} for details.

    \since QXmpp 1.1
*/
QString QXmppMessage::attachId() const
{
    return d->attachId;
}

/*!
    Sets the id of the attached message as in \xep{0367}{Message Attaching}.
    This can be used for a "reply to" or "reaction" function.

    The used message id depends on the message context, see the Business rules
    section of the XEP for details about when to use which id.

    \since QXmpp 1.1

    \a attachId.
*/
void QXmppMessage::setAttachId(const QString &attachId)
{
    d->attachId = attachId;
}

/*!
    Returns the participant ID of the sender if the message is received from a MIX channel as
    specified in \xep{0369}{Mediated Information eXchange (MIX)}.

    \since QXmpp 1.7
*/
QString QXmppMessage::mixParticipantId() const
{
    return mixUserJid().isEmpty() && mixUserNick().isEmpty() ? QString() : QXmppUtils::jidToResource(from());
}

/*!
    Returns the actual JID of a MIX channel participant.

    \since QXmpp 1.1
*/
QString QXmppMessage::mixUserJid() const
{
    return d->mixUserJid;
}

/*!
    Sets the actual JID of a MIX channel participant.

    \since QXmpp 1.1

    \a mixUserJid.
*/
void QXmppMessage::setMixUserJid(const QString &mixUserJid)
{
    d->mixUserJid = mixUserJid;
}

/*!
    Returns the MIX participant's nickname.

    \since QXmpp 1.1
*/
QString QXmppMessage::mixUserNick() const
{
    return d->mixUserNick;
}

/*!
    Sets the MIX participant's nickname.

    \since QXmpp 1.1

    \a mixUserNick.
*/
void QXmppMessage::setMixUserNick(const QString &mixUserNick)
{
    d->mixUserNick = mixUserNick;
}

/*!
    Returns the encryption method this message is advertised to be encrypted
    with.

    \note QXmppMessage::NoEncryption does not necesserily mean that the message
    is not encrypted; it may also be that the author of the message does not
    support \xep{0380}{Explicit Message Encryption}.

    \note If this returns QXmppMessage::UnknownEncryption, you can still get
    the namespace of the encryption with \c encryptionMethodNs() and possibly
    also a name with \c encryptionName().

    \since QXmpp 1.1
*/
QXmpp::EncryptionMethod QXmppMessage::encryptionMethod() const
{
    if (d->encryptionMethod.isEmpty()) {
        return QXmpp::NoEncryption;
    }
    return Enums::fromString<EncryptionMethod>(d->encryptionMethod).value_or(QXmpp::UnknownEncryption);
}

/*!
    Advertises that this message is encrypted with the given encryption method.
    See \xep{0380}{Explicit Message Encryption} for details.

    \since QXmpp 1.1

    \a method.
*/
void QXmppMessage::setEncryptionMethod(QXmpp::EncryptionMethod method)
{
    d->encryptionMethod = Enums::toString(method);
}

/*!
    Returns the namespace of the advertised encryption method via. \xep{0380}{Explicit Message Encryption}.

    \since QXmpp 1.1
*/
QString QXmppMessage::encryptionMethodNs() const
{
    return d->encryptionMethod;
}

/*!
    Sets the namespace of the encryption method this message advertises to be
    encrypted with. See \xep{0380}{Explicit Message Encryption} for details.

    \since QXmpp 1.1

    \a encryptionMethod.
*/
void QXmppMessage::setEncryptionMethodNs(const QString &encryptionMethod)
{
    d->encryptionMethod = encryptionMethod;
}

/*!
    Returns the associated name of the encryption method this message
    advertises to be encrypted with. See \xep{0380}{Explicit Message Encryption}
    for details.

    \since QXmpp 1.1
*/
QString QXmppMessage::encryptionName() const
{
    if (!d->encryptionName.isEmpty()) {
        return d->encryptionName;
    }
    return staticString(encryptionToName(encryptionMethod()));
}

/*!
    Sets the name of the encryption method for \xep{0380}{Explicit Message Encryption}.

    \note This should only be used, if the encryption method is custom and is
    not one of the methods listed in the XEP.

    \since QXmpp 1.1

    \a encryptionName.
*/
void QXmppMessage::setEncryptionName(const QString &encryptionName)
{
    d->encryptionName = encryptionName;
}

/*!
    Returns true, if this is a spoiler message according to \xep{0382}{Spoiler Messages}.
    The spoiler hint however can still be empty.

    A spoiler message's content should not be visible to the user by default.

    \since QXmpp 1.1
*/
bool QXmppMessage::isSpoiler() const
{
    return d->isSpoiler;
}

/*!
    Sets whether this is a spoiler message as specified in \xep{0382}{Spoiler Messages}.

    The content of spoiler messages will not be displayed by default to the
    user. However, clients not supporting spoiler messages will still display
    the content as usual.

    \since QXmpp 1.1

    \a isSpoiler.
*/
void QXmppMessage::setIsSpoiler(bool isSpoiler)
{
    d->isSpoiler = isSpoiler;
}

/*!
    Returns the spoiler hint as specified in \xep{0382}{Spoiler Messages}.

    The hint may be empty, even if isSpoiler is true.

    \since QXmpp 1.1
*/
QString QXmppMessage::spoilerHint() const
{
    return d->spoilerHint;
}

/*!
    Sets a spoiler hint for \xep{0382}{Spoiler Messages}. If the spoiler hint
    is not empty, isSpoiler will be set to true.

    A spoiler hint is optional for spoiler messages.

    Keep in mind that the spoiler hint is not displayed at all by clients not
    supporting spoiler messages.

    \since QXmpp 1.1

    \a spoilerHint.
*/
void QXmppMessage::setSpoilerHint(const QString &spoilerHint)
{
    d->spoilerHint = spoilerHint;
    if (!spoilerHint.isEmpty()) {
        d->isSpoiler = true;
    }
}

#ifdef BUILD_OMEMO
/*!
    Returns an included OMEMO element as defined by \xep{0384}{OMEMO Encryption}.

    \since QXmpp 1.5
*/
std::optional<QXmppOmemoElement> QXmppMessage::omemoElement() const
{
    return d->omemoElement;
}

/*!
    Sets an OMEMO element as defined by \xep{0384}{OMEMO Encryption}.

    \since QXmpp 1.5

    \a omemoElement.
*/
void QXmppMessage::setOmemoElement(const std::optional<QXmppOmemoElement> &omemoElement)
{
    d->omemoElement = omemoElement;
}
#endif

/*!
    Returns an included \xep{0369}{Mediated Information eXchange (MIX)}
    invitation as defined by
    \xep{0407}{Mediated Information eXchange (MIX): Miscellaneous Capabilities}.

    \since QXmpp 1.4
*/
std::optional<QXmppMixInvitation> QXmppMessage::mixInvitation() const
{
    return d->mixInvitation;
}

/*!
    Sets a \xep{0369}{Mediated Information eXchange (MIX)} invitation as defined
    by \xep{0407}{Mediated Information eXchange (MIX): Miscellaneous Capabilities}.

    \since QXmpp 1.4

    \a mixInvitation.
*/
void QXmppMessage::setMixInvitation(const std::optional<QXmppMixInvitation> &mixInvitation)
{
    d->mixInvitation = mixInvitation;
}

/*!
    Returns the MUC Occupant ID as defined in \xep{0421}{Occupant identifiers for semi-anonymous
    MUCs}.

    \since QXmpp 1.13
*/
QString QXmppMessage::mucOccupantId() const
{
    return d->mucOccupantId;
}

/*!
    Sets the MUC Occupant ID as defined in \xep{0421}{Occupant identifiers for semi-anonymous
    MUCs}.

    \since QXmpp 1.13

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

/*!
    Sets whether this message is only a fallback according to \xep{0428}{Fallback Indication}.

    This is useful for clients not supporting end-to-end encryption to indicate
    that the message body does not contain the intended text of the author.

    \deprecated Use fallbackMarkers() instead, it provides full access to the fallback elements.

    \since QXmpp 1.3
*/
bool QXmppMessage::isFallback() const
{
    return !d->fallbackMarkers.empty();
}

/*!
    Sets whether this message is only a fallback according to \xep{0428}{Fallback Indication}.

    This is useful for clients not supporting end-to-end encryption to indicate
    that the message body does not contain the intended text of the author.

    \deprecated Use setFallbackMarkers() instead, it provides full access to the fallback elements.

    \since QXmpp 1.3
*/
void QXmppMessage::setIsFallback(bool isFallback)
{
    if (isFallback) {
        d->fallbackMarkers = { QXmppFallback { {}, {} } };
    } else {
        d->fallbackMarkers.clear();
    }
}

/*!
    Returns the fallback elements defined in \xep{0428}{Fallback Indication}.

    \since QXmpp 1.7
*/
const QVector<QXmppFallback> &QXmppMessage::fallbackMarkers() const
{
    return d->fallbackMarkers;
}

/*!
    Sets the fallback elements defined in \xep{0428}{Fallback Indication}.

    \since QXmpp 1.7

    \a fallbackMarkers.
*/
void QXmppMessage::setFallbackMarkers(const QVector<QXmppFallback> &fallbackMarkers)
{
    d->fallbackMarkers = fallbackMarkers;
}

/*!
    Returns the body or subject text of the message without parts that have a fallback marker
    with a supported namespace.

    \a element decides whether to use the body or the subject text.
    \a supportedNamespaces is the list of namespaces for which fallback markers
    should be filtered out.

    \since QXmpp 1.9
*/
QString QXmppMessage::readFallbackRemovedText(QXmppFallback::Element element, const QVector<QString> &supportedNamespaces) const
{
    // filter out all QXmppFallback::Reference s
    auto markers = d->fallbackMarkers |
        views::filter([&](const auto &marker) { return contains(supportedNamespaces, marker.forNamespace()); }) |
        views::transform([](auto &&marker) { return marker.references(); });

    // collect references with correct namespace and element type (body or subject)
    QVector<QXmppFallback::Range> references;
    for (const auto &markerReferences : markers) {
        for (const auto &ref : markerReferences) {
            if (ref.element == element) {
                // early exit: element without range means whole text is fallback
                if (!ref.range) {
                    return {};
                }
                references.push_back(ref.range.value());
            }
        }
    }

    // sort by begin of fallback
    std::ranges::sort(references, {}, &QXmppFallback::Range::start);

    const QString fullText = element == QXmppFallback::Subject ? subject() : d->body;
    QString output;
    qsizetype index = 0;
    for (const auto &range : std::as_const(references)) {
        if (!(range.start < fullText.size()) || !(range.end > 0 && range.end <= fullText.size())) {
            // skip markers with invalid start/begin
            continue;
        }

        // fallback marker marks [start, end)
        // we want to copy the section before the marker: [index, start)

        if (index < range.start) {
            output.append(QStringView(fullText.data() + index, fullText.data() + range.start));
        }
        index = range.end;
    }
    // append rest of the string (after the last fallback marker)
    if (index < fullText.size()) {
        output.append(QStringView(fullText.data() + index, fullText.data() + fullText.size()));
    }

    return output;
}

/*!
    Returns the parts of the body or subject that are marked as fallback for
    \a forNamespace.

    \a element decides whether to use the body or the subject text.

    \since QXmpp 1.9
*/
QString QXmppMessage::readFallbackText(QXmppFallback::Element element, QStringView forNamespace) const
{
    const QString fullText = element == QXmppFallback::Subject ? subject() : d->body;

    // filter out all QXmppFallback::Reference s
    auto markers = d->fallbackMarkers |
        views::filter([&](const auto &marker) { return marker.forNamespace() == forNamespace; }) |
        views::transform([](auto &&marker) { return marker.references(); });

    // collect references with correct namespace and element type (body or subject)
    QVector<QXmppFallback::Range> references;
    for (const auto &markerReferences : markers) {
        for (const auto &ref : markerReferences) {
            if (ref.element == element) {
                // early exit: element without range means whole text is fallback
                if (!ref.range) {
                    return fullText;
                }
                references.push_back(ref.range.value());
            }
        }
    }

    // sort by begin of fallback
    std::ranges::sort(references, {}, &QXmppFallback::Range::start);

    QString output;
    for (const auto &range : references) {
        output.append(QStringView(fullText.data() + range.start, fullText.data() + range.end));
    }
    return output;
}

/*!
    Returns an included trust message element as defined by
    \xep{0434}{Trust Messages (TM)}.

    \since QXmpp 1.5
*/
std::optional<QXmppTrustMessageElement> QXmppMessage::trustMessageElement() const
{
    return d->trustMessageElement;
}

/*!
    Sets a trust message element as defined by \xep{0434}{Trust Messages (TM)}.

    \since QXmpp 1.5

    \a trustMessageElement.
*/
void QXmppMessage::setTrustMessageElement(const std::optional<QXmppTrustMessageElement> &trustMessageElement)
{
    d->trustMessageElement = trustMessageElement;
}

/*!
    Returns a reaction to a message as defined by \xep{0444}{Message Reactions}.

    \since QXmpp 1.5
*/
std::optional<QXmppMessageReaction> QXmppMessage::reaction() const
{
    return d->reaction;
}

/*!
    Sets a reaction to a message as defined by \xep{0444}{Message Reactions}.

    The corresponding hint must be set manually:
    \code
    QXmppMessage message;
    message.addHint(QXmppMessage::Store);
    \endcode

    \since QXmpp 1.5

    \a reaction.
*/
void QXmppMessage::setReaction(const std::optional<QXmppMessageReaction> &reaction)
{
    d->reaction = reaction;
}

/*!
    Returns the via \xep{0447}{Stateless file sharing} shared files attached to this message.

    \since QXmpp 1.5
*/
const QVector<QXmppFileShare> &QXmppMessage::sharedFiles() const
{
    return d->sharedFiles;
}

/*!
    Sets the via \xep{0447}{Stateless file sharing} shared files attached to this message.

    \since QXmpp 1.5

    \a sharedFiles.
*/
void QXmppMessage::setSharedFiles(const QVector<QXmppFileShare> &sharedFiles)
{
    d->sharedFiles = sharedFiles;
}

/*!
    Returns additional sources to be attached to a file share as defined by \xep{0447}{Stateless
    file sharing}.

    \since QXmpp 1.7
*/
QVector<QXmppFileSourcesAttachment> QXmppMessage::fileSourcesAttachments() const
{
    return d->fileSourcesAttachments;
}

/*!
    Sets additional sources to be attached to a file share as defined by \xep{0447}{Stateless
    file sharing}.

    \since QXmpp 1.7

    \a fileSourcesAttachments.
*/
void QXmppMessage::setFileSourcesAttachments(const QVector<QXmppFileSourcesAttachment> &fileSourcesAttachments)
{
    d->fileSourcesAttachments = fileSourcesAttachments;
}

/*!
    Returns the message reply extension as defined in \xep{0461}{Message Replies}.

    \since QXmpp 1.9
*/
std::optional<QXmpp::Reply> QXmppMessage::reply() const
{
    return d->reply;
}

/*!
    Sets the message reply extension as defined in \xep{0461}{Message Replies}.

    \since QXmpp 1.9

    \a reply.
*/
void QXmppMessage::setReply(const std::optional<QXmpp::Reply> &reply)
{
    d->reply = reply;
}

/*!
    Returns the part of the body that is marked as fallback for \xep{0461}{Message Replies},
    without any quotation marks ('> ').

    \since QXmpp 1.9
*/
QString QXmppMessage::readReplyQuoteFromBody() const
{
    auto replyFallbackBody = readFallbackText(QXmppFallback::Body, staticString(ns_reply));
    auto lines = replyFallbackBody.split(u'\n');
    // remove '> ' quotation
    for (auto &line : lines) {
        if (line == u'>') {
            line = QString();
        } else if (line.startsWith(u"> ")) {
            line = line.mid(QStringView(u"> ").size());
        }
    }
    return lines.join(u'\n');
}

/*! Returns a Call Invite element as defined in \xep{0482}{Call Invites}. */
std::optional<QXmppCallInviteElement> QXmppMessage::callInviteElement() const
{
    return d->callInviteElement;
}

/*!
    Sets a Call Invite element as defined in \xep{0482}{Call Invites}.

    \a callInviteElement.
*/
void QXmppMessage::setCallInviteElement(std::optional<QXmppCallInviteElement> callInviteElement)
{
    d->callInviteElement = callInviteElement;
}

/*!
    Returns the typed extension container for this message.

    Extensions parsed via QXmpp::Xml::Registry and unknown elements stored as
    QXmpp::Xml::Element are accessible here. This method hides
    QXmppStanza::extensions(); application code should migrate to this API.

    \since QXmpp 1.16
*/
QXmpp::Xml::Extensions &QXmppMessage::extensions()
{
    return d->extensions;
}

/*! \overload */
const QXmpp::Xml::Extensions &QXmppMessage::extensions() const
{
    return d->extensions;
}

void QXmppMessage::parse(const QDomElement &element)
{
    parse(element, QXmpp::SceAll);
}

void QXmppMessage::parse(const QDomElement &element, QXmpp::SceMode sceMode)
{
    QXmppStanza::parse(element);
    d->type = Enums::fromString<Type>(element.attribute(u"type"_s)).value_or(Normal);
    parseExtensions(element, sceMode);
}

void QXmppMessage::toXml(QXmlStreamWriter *writer) const
{
    toXml(writer, QXmpp::SceAll);
}

void QXmppMessage::toXml(QXmlStreamWriter *writer, QXmpp::SceMode sceMode) const
{
    XmlWriter(writer).write(Element {
        u"message",
        OptionalAttribute { u"xml:lang", lang() },
        OptionalAttribute { u"id", id() },
        OptionalAttribute { u"to", to() },
        OptionalAttribute { u"from", from() },
        Attribute { u"type", d->type },
        errorOptional(),
        [&] {
            // known extensions
            serializeExtensions(writer, sceMode);

            // other, unknown extensions
            QXmppStanza::extensionsToXml(writer);
        },
    });
}

// Parses all child elements of a message stanza.
//
// element is the message element or SCE content element. sceMode is the mode
// to decide which child elements of the message to parse.
void QXmppMessage::parseExtensions(const QDomElement &element, const QXmpp::SceMode sceMode)
{
    QXmppElementList unknownExtensions;
    for (const auto &childElement : iterChildElements(element)) {
        if (!checkElement(childElement, u"addresses", ns_extended_addressing) &&
            childElement.tagName() != u"error") {
            // Try to parse the element and add it as an unknown extension if it
            // fails.
            if (!parseExtension(childElement, sceMode)) {
                unknownExtensions << QXmppElement(childElement);
            }
        }
    }
    setExtensions(unknownExtensions);
}

/*!
    Parses a child element of the message stanza.

    Allows inherited classes to parse additional extension elements. This
    function may be executed multiple times with different elements.

    \a element is the child element of the message to be parsed.
    \a sceMode decides which elements to be parsed from the DOM (all known /
    only public / only sensitive).

    Returns true, if the element was successfully parsed.

    \since QXmpp 1.5
*/
bool QXmppMessage::parseExtension(const QDomElement &element, QXmpp::SceMode sceMode)
{
    if (sceMode & QXmpp::ScePublic) {
        if (sceMode == QXmpp::ScePublic && element.tagName() == u"body") {
            d->e2eeFallbackBody = element.text();
            return true;
        }

        // XEP-0280: Message Carbons
        if (checkElement(element, u"private", ns_carbons)) {
            d->privatemsg = true;
            return true;
        }
        // XEP-0334: Message Processing Hints
        if (element.namespaceURI() == ns_message_processing_hints) {
            if (auto type = Enums::fromString<Hint>(element.tagName())) {
                addHint(*type);
            }
            return true;
        }
        // XEP-0353: Jingle Message Initiation
        if (QXmppJingleMessageInitiationElement::isJingleMessageInitiationElement(element)) {
            d->jingleMessageInitiationElement = parseOptionalElement<QXmppJingleMessageInitiationElement>(element);
            return true;
        }
        // XEP-0359: Unique and Stable Stanza IDs
        if (checkElement(element, u"stanza-id", ns_sid)) {
            d->stanzaIds.push_back(QXmppStanzaId {
                element.attribute(u"id"_s),
                element.attribute(u"by"_s),
            });
            return true;
        }
        if (checkElement(element, u"origin-id", ns_sid)) {
            d->originId = element.attribute(u"id"_s);
            return true;
        }
        // XEP-0369: Mediated Information eXchange (MIX)
        if (checkElement(element, u"mix", ns_mix)) {
            d->mixUserJid = element.firstChildElement(u"jid"_s).text();
            d->mixUserNick = element.firstChildElement(u"nick"_s).text();
            return true;
        }
        // XEP-0380: Explicit Message Encryption
        if (checkElement(element, u"encryption", ns_eme)) {
            d->encryptionMethod = element.attribute(u"namespace"_s);
            d->encryptionName = element.attribute(u"name"_s);
            return true;
        }
#ifdef BUILD_OMEMO
        // XEP-0384: OMEMO Encryption
        if (QXmppOmemoElement::isOmemoElement(element)) {
            d->omemoElement = parseOptionalElement<QXmppOmemoElement>(element);
            return true;
        }
#endif
        // XEP-0421: Occupant identifiers for semi-anonymous MUCs
        if (isElement<MucOccupantId>(element)) {
            d->mucOccupantId = element.attribute(u"id"_s);
            return true;
        }
        // XEP-0482: Call Invites
        if (QXmppCallInviteElement::isCallInviteElement(element)) {
            d->callInviteElement = parseOptionalElement<QXmppCallInviteElement>(element);
            return true;
        }
    }
    if (sceMode & QXmpp::SceSensitive) {
        if (element.tagName() == u"body") {
            d->body = element.text();
            return true;
        }
        if (element.tagName() == u"subject") {
            d->subject = element.text();
            return true;
        }
        if (element.tagName() == u"thread") {
            d->thread = element.text();
            d->parentThread = element.attribute(u"parent"_s);
            return true;
        }
        if (element.tagName() == u"x") {
            if (element.namespaceURI() == ns_legacy_delayed_delivery) {
                // if XEP-0203 exists, XEP-0091 has no need to parse because XEP-0091
                // is no more standard protocol)
                if (d->stamp.isNull()) {
                    // XEP-0091: Legacy Delayed Delivery
                    d->stamp = QDateTime::fromString(
                        element.attribute(u"stamp"_s),
                        u"yyyyMMddThh:mm:ss"_s);
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
                    d->stamp.setTimeZone(QTimeZone::Initialization::UTC);
#else
                    d->stamp.setTimeZone(QTimeZone(0));
#endif
                    d->stampType = LegacyDelayedDelivery;
                }
                return true;
            }
            // XEP-0045: Multi-User Chat
            if (element.namespaceURI() == ns_muc_user) {
                d->mucUserQuery = QXmpp::Muc::UserQuery::fromDom(element);
                return true;
            }
            // XEP-0045: Multi-User Chat voice request (muc#request form)
            if (element.namespaceURI() == ns_data) {
                QXmppDataForm form;
                form.parse(element);
                if (auto voiceRequest = QXmppMucVoiceRequest::fromDataForm(form)) {
                    d->mucVoiceRequest = std::move(voiceRequest);
                    return true;
                }
            }
            // XEP-0249: Direct MUC Invitations
            if (element.namespaceURI() == ns_conference) {
                d->mucDirectInvitation = Muc::DirectInvitation::fromDom(element);
                return true;
            }
            // XEP-0066: Out of Band Data
            if (element.namespaceURI() == ns_oob) {
                QXmppOutOfBandUrl data;
                data.parse(element);
                d->outOfBandUrls.push_back(std::move(data));
                return true;
            }
        }
        // XEP-0071: XHTML-IM
        if (checkElement(element, u"html", ns_xhtml_im)) {
            QDomElement bodyElement = element.firstChildElement(u"body"_s);
            if (!bodyElement.isNull() && bodyElement.namespaceURI() == ns_xhtml) {
                QTextStream stream(&d->xhtml, QIODevice::WriteOnly);
                bodyElement.save(stream, 0);

                d->xhtml = d->xhtml.mid(d->xhtml.indexOf(u'>') + 1);
                d->xhtml.replace(
                    u" xmlns=\"http://www.w3.org/1999/xhtml\""_s,
                    QString());
                d->xhtml.replace(u"</body>"_s, QString());
                d->xhtml = d->xhtml.trimmed();
            }
            return true;
        }
        // XEP-0085: Chat State Notifications
        if (element.namespaceURI() == ns_chat_states) {
            d->state = Enums::fromString<State>(element.tagName()).value_or(None);
            return true;
        }
        // XEP-0184: Message Delivery Receipts
        if (checkElement(element, u"received", ns_message_receipts)) {
            d->receiptId = element.attribute(u"id"_s);

            // compatibility with old-style XEP
            if (d->receiptId.isEmpty()) {
                d->receiptId = id();
            }
            return true;
        }
        if (checkElement(element, u"request", ns_message_receipts)) {
            d->receiptRequested = true;
            return true;
        }
        // XEP-0203: Delayed Delivery
        if (checkElement(element, u"delay", ns_delayed_delivery)) {
            d->stamp = QXmppUtils::datetimeFromString(
                element.attribute(u"stamp"_s));
            d->stampType = DelayedDelivery;
            return true;
        }
        // XEP-0224: Attention
        if (checkElement(element, u"attention", ns_attention)) {
            d->attentionRequested = true;
            return true;
        }
        // XEP-0231: Bits of Binary
        if (QXmppBitsOfBinaryData::isBitsOfBinaryData(element)) {
            QXmppBitsOfBinaryData data;
            data.parseElementFromChild(element);
            d->bitsOfBinaryData << data;
            return true;
        }
        // XEP-0308: Last Message Correction
        if (checkElement(element, u"replace", ns_message_correct)) {
            d->replaceId = element.attribute(u"id"_s);
            return true;
        }
        // XEP-0333: Chat Markers
        if (element.namespaceURI() == ns_chat_markers) {
            if (element.tagName() == u"markable") {
                d->markable = true;
            } else {
                if (auto marker = Enums::fromString<Marker>(element.tagName())) {
                    d->marker = *marker;
                    d->markedId = element.attribute(u"id"_s);
                    d->markedThread = element.attribute(u"thread"_s);
                }
            }
            return true;
        }
        // XEP-0367: Message Attaching
        if (checkElement(element, u"attach-to", ns_message_attaching)) {
            d->attachId = element.attribute(u"id"_s);
            return true;
        }
        // XEP-0382: Spoiler messages
        if (checkElement(element, u"spoiler", ns_spoiler)) {
            d->isSpoiler = true;
            d->spoilerHint = element.text();
            return true;
        }
        // XEP-0407: Mediated Information eXchange (MIX): Miscellaneous Capabilities
        if (checkElement(element, u"invitation", ns_mix_misc)) {
            d->mixInvitation = parseOptionalElement<QXmppMixInvitation>(element);
            return true;
        }
        // XEP-0434: Trust Messages (TM)
        if (QXmppTrustMessageElement::isTrustMessageElement(element)) {
            d->trustMessageElement = parseOptionalElement<QXmppTrustMessageElement>(element);
            return true;
        }
        // XEP-0444: Message Reactions
        if (QXmppMessageReaction::isMessageReaction(element)) {
            d->reaction = parseOptionalElement<QXmppMessageReaction>(element);
            return true;
        }
        // XEP-0447: Stateless file sharing
        if (checkElement(element, u"file-sharing", ns_sfs)) {
            QXmppFileShare share;
            if (share.parse(element)) {
                d->sharedFiles.push_back(std::move(share));
            }
            return true;
        }
        // XEP-0461: Message Replies
        if (checkElement(element, u"reply", ns_reply)) {
            d->reply = Reply {
                element.attribute(u"to"_s),
                element.attribute(u"id"_s),
            };
            return true;
        }
        if (checkElement(element, u"sources", ns_sfs)) {
            if (auto fileSources = QXmppFileSourcesAttachment::fromDom(element)) {
                d->fileSourcesAttachments.push_back(std::move(*fileSources));
            }
            return true;
        }
    }

    // read from both public and private extensions:

    // XEP-0428: Fallback Indication
    if (checkElement(element, u"fallback", ns_fallback_indication)) {
        if (auto fallback = QXmppFallback::fromDom(element)) {
            d->fallbackMarkers.push_back(std::move(*fallback));
        }
        return true;
    }
    return d->extensions.tryParseAndAdd(QXmpp::Xml::Scope::Message, element);
}

/*!
    Serializes all additional child elements.

    \a writer is the XML stream writer to output the XML.
    \a sceMode is the mode which decides which elements to output (only useful
    for encryption).
    \a baseNamespace is a custom namespace added to basic XMPP-Core elements
    like &lt;body/&gt; (needed when encrypting elements outside of the stream).

    \since QXmpp 1.5
*/
void QXmppMessage::serializeExtensions(QXmlStreamWriter *writer, QXmpp::SceMode sceMode, const QString &baseNamespace) const
{
    XmlWriter w(writer);
    if (sceMode & QXmpp::ScePublic) {
        if (sceMode == QXmpp::ScePublic && !d->e2eeFallbackBody.isEmpty()) {
            w.write(TextElement { u"body", d->e2eeFallbackBody });
        }

        // XEP-0280: Message Carbons
        if (d->privatemsg) {
            w.write(Element { { u"private", ns_carbons } });
        }

        // XEP-0334: Message Processing Hints
        for (auto hint : Enums::toStrings(d->hints)) {
            w.write(Element { { hint, ns_message_processing_hints } });
        }

        // XEP-0359: Unique and Stable Stanza IDs
        for (const auto &stanzaId : d->stanzaIds) {
            w.write(Element {
                { u"stanza-id", ns_sid },
                Attribute { u"id", stanzaId.id },
                OptionalAttribute { u"by", stanzaId.by },
            });
        }

        if (!d->originId.isNull()) {
            w.write(Element {
                { u"origin-id", ns_sid },
                Attribute { u"id", d->originId },
            });
        }

        // XEP-0369: Mediated Information eXchange (MIX)
        if (!d->mixUserJid.isEmpty() || !d->mixUserNick.isEmpty()) {
            w.write(Element {
                { u"mix", ns_mix },
                OptionalTextElement { u"jid", d->mixUserJid },
                OptionalTextElement { u"nick", d->mixUserNick },
            });
        }

        // XEP-0380: Explicit Message Encryption
        if (!d->encryptionMethod.isEmpty()) {
            w.write(Element {
                { u"encryption", ns_eme },
                Attribute { u"namespace", d->encryptionMethod },
                OptionalAttribute { u"name", d->encryptionName },
            });
        }

        // XEP-0421: Occupant identifiers for semi-anonymous MUCs
        if (!d->mucOccupantId.isEmpty()) {
            w.write(Element { MucOccupantId::XmlTag, Attribute { u"id", d->mucOccupantId } });
        }

#ifdef BUILD_OMEMO
        // XEP-0384: OMEMO Encryption
        w.write(d->omemoElement);
#endif
    }

    if (sceMode & QXmpp::SceSensitive) {
        // XMPP-Core
        if (baseNamespace.isEmpty()) {
            w.write(OptionalTextElement { u"subject", d->subject });
            w.write(OptionalTextElement { u"body", d->body });
            if (!d->thread.isEmpty()) {
                w.write(Element {
                    u"thread",
                    OptionalAttribute { u"parent", d->parentThread },
                    Characters { d->thread },
                });
            }
        } else {
            w.write(OptionalTextElement { { u"subject", baseNamespace }, d->subject });
            w.write(OptionalTextElement { { u"body", baseNamespace }, d->body });
            if (!d->thread.isEmpty()) {
                w.write(Element {
                    { u"thread", baseNamespace },
                    OptionalAttribute { u"parent", d->parentThread },
                    Characters { d->thread },
                });
            }
        }

        // XEP-0066: Out of Band Data
        w.write(d->outOfBandUrls);

        // XEP-0071: XHTML-IM
        if (!d->xhtml.isEmpty()) {
            w.write(Element {
                { u"html", ns_xhtml_im },
                Element {
                    { u"body", ns_xhtml },
                    Characters { QStringView() },
                    [&] { writer->device()->write(d->xhtml.toUtf8()); },
                },
            });
        }

        // XEP-0085: Chat State Notifications
        w.write(OptionalEnumElement { d->state, ns_chat_states });

        // XEP-0091: Legacy Delayed Delivery | XEP-0203: Delayed Delivery
        if (d->stamp.isValid()) {
            QDateTime utcStamp = d->stamp.toUTC();
            if (d->stampType == DelayedDelivery) {
                // XEP-0203: Delayed Delivery
                w.write(Element { { u"delay", ns_delayed_delivery }, Attribute { u"stamp", utcStamp } });
            } else {
                // XEP-0091: Legacy Delayed Delivery
                w.write(Element {
                    { u"x", ns_legacy_delayed_delivery },
                    Attribute { u"stamp", utcStamp.toString(u"yyyyMMddThh:mm:ss") },
                });
            }
        }

        // XEP-0184: Message Delivery Receipts
        // An ack message (message containing a "received" element) must not
        // include a receipt request ("request" element) in order to prevent
        // looping.
        if (!d->receiptId.isEmpty()) {
            w.write(Element { { u"received", ns_message_receipts }, Attribute { u"id", d->receiptId } });
        } else if (d->receiptRequested) {
            w.write(Element { { u"request", ns_message_receipts } });
        }

        // XEP-0224: Attention
        if (d->attentionRequested) {
            w.write(Element { { u"attention", ns_attention } });
        }

        // XEP-0045: Multi-User Chat
        if (d->mucUserQuery) {
            d->mucUserQuery->toXml(writer);
        }
        if (d->mucVoiceRequest) {
            d->mucVoiceRequest->toDataForm().toXml(writer);
        }

        // XEP-0249: Direct MUC Invitations
        if (d->mucDirectInvitation) {
            d->mucDirectInvitation->toXml(writer);
        }

        // XEP-0231: Bits of Binary
        for (const auto &data : std::as_const(d->bitsOfBinaryData)) {
            data.toXmlElementFromChild(writer);
        }

        // XEP-0308: Last Message Correction
        if (!d->replaceId.isEmpty()) {
            w.write(Element { { u"replace", ns_message_correct }, Attribute { u"id", d->replaceId } });
        }

        // XEP-0333: Chat Markers
        if (d->markable) {
            w.write(Element { { u"markable", ns_chat_markers } });
        }
        if (d->marker != NoMarker) {
            w.write(Element {
                Tag { d->marker, ns_chat_markers },
                Attribute { u"id", d->markedId },
                OptionalAttribute { u"thread", d->markedThread },
            });
        }

        // XEP-0353: Jingle Message Initiation
        w.write(d->jingleMessageInitiationElement);

        // XEP-0367: Message Attaching
        if (!d->attachId.isEmpty()) {
            w.write(Element { { u"attach-to", ns_message_attaching }, Attribute { u"id", d->attachId } });
        }

        // XEP-0382: Spoiler messages
        if (d->isSpoiler) {
            w.write(TextElement { { u"spoiler", ns_spoiler }, d->spoilerHint });
        }

        // XEP-0407: Mediated Information eXchange (MIX): Miscellaneous Capabilities
        w.write(d->mixInvitation);

        // XEP-0434: Trust Messages (TM)
        w.write(d->trustMessageElement);

        // XEP-0444: Message Reactions
        w.write(d->reaction);

        // XEP-0447: Stateless file sharing
        w.write(d->sharedFiles);
        w.write(d->fileSourcesAttachments);

        // XEP-0461: Message Replies
        if (d->reply) {
            w.write(Element {
                { u"reply", ns_reply },
                OptionalAttribute { u"to", d->reply->to },
                Attribute { u"id", d->reply->id },
            });
        }

        // XEP-0482: Call Invites
        w.write(d->callInviteElement);
    }

    // serialize in private and in public part

    // XEP-0428: Fallback Indication
    // fallback markers may be used in the private part (e.g. message replies) but also in the
    // public part (e.g. the fallback body for e2ee messages)
    w.write(d->fallbackMarkers);

    d->extensions.toXml(writer);
}

struct QXmppFallbackPrivate : QSharedData {
    QString forNamespace;
    QVector<QXmppFallback::Reference> references;
};

/*!
    \class QXmppFallback
    \inmodule QXmpp

    Fallback marker for message stanzas as defined in \xep{0428}{Fallback Indication}.

    \sa QXmppFallback
    \since QXmpp 1.7
*/

/*!
    \enum QXmppFallback::Element

    Describes the element of the message stanza this refers to.

    \value Body The fallback refers to the message body.
    \value Subject The fallback refers to the message subject.
*/

/*!
    \struct QXmppFallback::Range
    \inmodule QXmpp

    A character range of a string, see \xep{0426}{Character counting in message bodies} for details.
*/

/*!
    \struct QXmppFallback::Reference
    \inmodule QXmpp

    A reference to a text in the message stanza.
*/

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppFallback)

/*! Creates a fallback marker. */
QXmppFallback::QXmppFallback(const QString &forNamespace, const QVector<Reference> &references)
    : d(new QXmppFallbackPrivate { {}, forNamespace, references })
{
}

/*! Returns the namespace of the XEP that this fallback marker is referring to. */
const QString &QXmppFallback::forNamespace() const
{
    return d->forNamespace;
}

/*!
    Sets the namespace of the XEP that this fallback marker is referring to.

    \a ns.
*/
void QXmppFallback::setForNamespace(const QString &ns)
{
    d->forNamespace = ns;
}

/*! Returns the text references of this fallback marker. */
const QVector<QXmppFallback::Reference> &QXmppFallback::references() const
{
    return d->references;
}

/*! Sets the text \a references of this fallback marker. */
void QXmppFallback::setReferences(const QVector<Reference> &references)
{
    d->references = references;
}

/*!
    Tries to parse \a el into a QXmppFallback object.

    Returns an empty optional on failure, the parsed object otherwise.
*/
std::optional<QXmppFallback> QXmppFallback::fromDom(const QDomElement &el)
{
    if (el.tagName() != u"fallback" || el.namespaceURI() != ns_fallback_indication) {
        return {};
    }

    QVector<Reference> references;
    for (const auto &subEl : iterChildElements(el, {}, ns_fallback_indication)) {
        auto start = parseInt<uint32_t>(subEl.attribute(u"start"_s));
        auto end = parseInt<uint32_t>(subEl.attribute(u"end"_s));
        std::optional<Range> range;
        if (start && end) {
            range = Range { *start, *end };
        }

        if (subEl.tagName() == u"body") {
            references.push_back(Reference { Body, range });
        } else if (subEl.tagName() == u"subject") {
            references.push_back(Reference { Subject, range });
        }
    }

    return QXmppFallback {
        el.attribute(u"for"_s),
        references,
    };
}

/*!
    Serializes the object to XML.

    \a writer.
*/
void QXmppFallback::toXml(QXmlStreamWriter *writer) const
{
    XmlWriter w(writer);
    w.write(::Element {
        { u"fallback", ns_fallback_indication },
        OptionalAttribute { u"for", d->forNamespace },
        [&] {
            for (const auto &reference : d->references) {
                w.write(::Element {
                    QStringView(reference.element == Body ? u"body" : u"subject"),
                    [&] {
                        if (reference.range) {
                            w.write(Attribute { u"start", reference.range->start });
                            w.write(Attribute { u"end", reference.range->end });
                        }
                    },
                });
            }
        },
    });
}
