// SPDX-FileCopyrightText: 2019 Linus Jahn <lnj@kaidan.im>
// SPDX-FileCopyrightText: 2019 Melvin Keskin <melvo@olomono.de>
// SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb@kaidan.im>
//
// SPDX-License-Identifier: LGPL-2.1-or-later

#include "QXmppUri.h"

#include "QXmppMessage_p.h"
#include "QXmppUtils_p.h"

#include "StringLiterals.h"

#include <QUrlQuery>

using namespace QXmpp::Private;
using namespace QXmpp::Uri;

constexpr QStringView SCHEME = u"xmpp";
constexpr QChar QUERY_ITEM_DELIMITER = u';';
constexpr QChar QUERY_ITEM_KEY_DELIMITER = u'=';

// Adds a key-value pair to a query if the value is not empty.
static void addKeyValuePairToQuery(QUrlQuery &query, const QString &key, QStringView value)
{
    if (!value.isEmpty()) {
        query.addQueryItem(key, value.toString());
    }
}

// Extracts the fully-encoded value of a query's key-value pair.
static QString queryItemValue(const QUrlQuery &query, const QString &key)
{
    return query.queryItemValue(key, QUrl::FullyDecoded);
}

/*!
    \namespace QXmpp::Uri
    \inmodule QXmpp

    Contains URI classes that can be serialized to URI queries (see QXmppUri).

    \since QXmpp 1.8
*/

namespace QXmpp::Uri {

/*!
    \struct QXmpp::Uri::Command
    \inmodule QXmpp

    A "command" query from \xep{0050}{Ad-Hoc Commands}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Invite
    \inmodule QXmpp

    An "invite" query from \xep{0045}{Multi-User Chat}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Join
    \inmodule QXmpp

    A "join" query from \xep{0045}{Multi-User Chat}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Login
    \inmodule QXmpp

    A "login" query, not formally specified.

    Used in the wild, e.g. by Kaidan.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Message
    \inmodule QXmpp

    A "message" query defined in \xep{0147}{XMPP URI Scheme Query Components}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Unregister
    \inmodule QXmpp

    An "unregister" query defined in \xep{0077}{In-Band Registration}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Unsubscribe
    \inmodule QXmpp

    An "unsubscribe" query defined in \xep{0147}{XMPP URI Scheme Query Components}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Register
    \inmodule QXmpp

    A "register" query defined in \xep{0077}{In-Band Registration}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Remove
    \inmodule QXmpp

    A "remove" query defined in \xep{0147}{XMPP URI Scheme Query Components}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Roster
    \inmodule QXmpp

    A "roster" query defined in \xep{0147}{XMPP URI Scheme Query Components}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::Subscribe
    \inmodule QXmpp

    A "subscribe" query defined in \xep{0147}{XMPP URI Scheme Query Components}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::TrustMessage
    \inmodule QXmpp

    A "trust-message" query defined in \xep{0434}{Trust Messages (TM)}.

    \since QXmpp 1.8
*/

/*!
    \struct QXmpp::Uri::CustomQuery
    \inmodule QXmpp

    A query with a custom name and custom key-value pairs.

    Queries will be parsed into this type if they are unknown.

    \since QXmpp 1.8
*/

}  // namespace QXmpp::Uri

static void serializeUrlQuery(const Command &command, QUrlQuery &query)
{
    query.addQueryItem(u"command"_s, {});

    addKeyValuePairToQuery(query, u"node"_s, command.node);
    addKeyValuePairToQuery(query, u"action"_s, command.action);
}

static void serializeUrlQuery(const Invite &invite, QUrlQuery &query)
{
    query.addQueryItem(u"invite"_s, {});

    addKeyValuePairToQuery(query, u"jid"_s, invite.inviteeJid);
    addKeyValuePairToQuery(query, u"password"_s, invite.password);
}

static void serializeUrlQuery(const Join &join, QUrlQuery &query)
{
    query.addQueryItem(u"join"_s, {});

    addKeyValuePairToQuery(query, u"password"_s, join.password);
}

static void serializeUrlQuery(const Login &login, QUrlQuery &query)
{
    query.addQueryItem(u"login"_s, {});

    addKeyValuePairToQuery(query, u"password"_s, login.password);
}

static void serializeUrlQuery(const Message &message, QUrlQuery &query)
{
    query.addQueryItem(u"message"_s, {});

    addKeyValuePairToQuery(query, u"from"_s, message.from);
    addKeyValuePairToQuery(query, u"id"_s, message.id);
    if (message.type) {
        addKeyValuePairToQuery(query, u"type"_s, Enums::toString(*message.type));
    }
    addKeyValuePairToQuery(query, QStringLiteral("subject"), message.subject);
    addKeyValuePairToQuery(query, QStringLiteral("body"), message.body);
    addKeyValuePairToQuery(query, QStringLiteral("thread"), message.thread);
}

static void serializeUrlQuery(const Unregister &, QUrlQuery &query)
{
    query.addQueryItem(u"unregister"_s, {});
}

static void serializeUrlQuery(const Unsubscribe &, QUrlQuery &query)
{
    query.addQueryItem(u"unsubscribe"_s, {});
}

static void serializeUrlQuery(const Register &, QUrlQuery &query)
{
    query.addQueryItem(u"register"_s, {});
}

static void serializeUrlQuery(const Remove &, QUrlQuery &query)
{
    query.addQueryItem(u"remove"_s, {});
}

static void serializeUrlQuery(const Roster &roster, QUrlQuery &query)
{
    query.addQueryItem(u"roster"_s, {});

    addKeyValuePairToQuery(query, u"name"_s, roster.name);
    addKeyValuePairToQuery(query, u"group"_s, roster.group);
}

static void serializeUrlQuery(const Subscribe &, QUrlQuery &query)
{
    query.addQueryItem(u"subscribe"_s, {});
}

static void serializeUrlQuery(const TrustMessage &trustMessage, QUrlQuery &query)
{
    query.addQueryItem(u"trust-message"_s, {});

    addKeyValuePairToQuery(query, QStringLiteral("encryption"), trustMessage.encryption);

    for (auto &identifier : trustMessage.trustKeyIds) {
        addKeyValuePairToQuery(query, QStringLiteral("trust"), identifier);
    }

    for (auto &identifier : trustMessage.distrustKeyIds) {
        addKeyValuePairToQuery(query, QStringLiteral("distrust"), identifier);
    }
}

static void serializeUrlQuery(const CustomQuery &custom, QUrlQuery &query)
{
    query.addQueryItem(custom.query, {});
    for (const auto &[key, value] : custom.parameters) {
        query.addQueryItem(key, value);
    }
}

Command parseCommandQuery(const QUrlQuery &q)
{
    return {
        queryItemValue(q, u"node"_s),
        queryItemValue(q, u"action"_s),
    };
}

Invite parseInviteQuery(const QUrlQuery &q)
{
    return {
        queryItemValue(q, u"jid"_s),
        queryItemValue(q, u"password"_s),
    };
}

Join parseJoinQuery(const QUrlQuery &q)
{
    return {
        queryItemValue(q, u"password"_s),
    };
}

Login parseLoginQuery(const QUrlQuery &q)
{
    return {
        queryItemValue(q, u"password"_s),
    };
}

Message parseMessageQuery(const QUrlQuery &q)
{
    return {
        queryItemValue(q, u"subject"_s),
        queryItemValue(q, u"body"_s),
        queryItemValue(q, u"thread"_s),
        queryItemValue(q, u"id"_s),
        queryItemValue(q, u"from"_s),
        Enums::fromString<QXmppMessage::Type>(queryItemValue(q, u"type"_s)),
    };
}

Roster parseRosterQuery(const QUrlQuery &q)
{
    return Roster {
        queryItemValue(q, u"name"_s),
        queryItemValue(q, u"group"_s),
    };
}

TrustMessage parseTrustMessageQuery(const QUrlQuery &q)
{
    return TrustMessage {
        queryItemValue(q, QStringLiteral("encryption")),
        q.allQueryItemValues(QStringLiteral("trust"), QUrl::FullyDecoded),
        q.allQueryItemValues(QStringLiteral("distrust"), QUrl::FullyDecoded),
    };
}

CustomQuery parseCustomQuery(const QUrlQuery &q)
{
    auto queryItems = q.queryItems();
    auto queryName = queryItems.first().first;
    queryItems.pop_front();
    return CustomQuery { queryName, queryItems };
}

struct QXmppUriPrivate : QSharedData {
    QString jid;
    std::any query;
};

/*! Creates an empty XMPP URI */
QXmppUri::QXmppUri()
    : d(new QXmppUriPrivate)
{
}

QXMPP_PRIVATE_DEFINE_RULE_OF_SIX(QXmppUri)

/*! Parses an XMPP URI from the string \a input.

    Returns the parsed URI or an error if the string could not be parsed. Returns true on success. */
std::variant<QXmppUri, QXmppError> QXmppUri::fromString(const QString &input)
{
    QUrl url(input);
    if (!url.isValid()) {
        return QXmppError { u"Invalid URI"_s, {} };
    }
    if (url.scheme() != SCHEME) {
        return QXmppError { u"Wrong URI scheme (is '%1', must be xmpp)"_s.arg(url.scheme()), {} };
    }

    QXmppUri uri;
    uri.setJid(url.path());

    if (url.hasQuery()) {
        QUrlQuery urlQuery;
        urlQuery.setQueryDelimiters(QUERY_ITEM_KEY_DELIMITER, QUERY_ITEM_DELIMITER);
        urlQuery.setQuery(url.query(QUrl::FullyEncoded));

        // Check if there is at least one query item.
        if (!urlQuery.isEmpty()) {
            auto queryItems = urlQuery.queryItems();
            Q_ASSERT(!queryItems.isEmpty());
            auto [queryString, queryValue] = queryItems.first();

            if (!queryValue.isEmpty()) {
                // invalid XMPP URI: first query query pair must have only key, no value
                return QXmppError { u"Invalid URI query: got key-value pair (instead of key only) for first query parameter."_s, {} };
            }

            if (queryString == u"command") {
                uri.d->query = parseCommandQuery(urlQuery);
            } else if (queryString == u"invite") {
                uri.d->query = parseInviteQuery(urlQuery);
            } else if (queryString == u"join") {
                uri.d->query = parseJoinQuery(urlQuery);
            } else if (queryString == u"login") {
                uri.d->query = parseLoginQuery(urlQuery);
            } else if (queryString == u"message") {
                uri.d->query = parseMessageQuery(urlQuery);
            } else if (queryString == u"register") {
                uri.d->query = Register();
            } else if (queryString == u"remove") {
                uri.d->query = Remove();
            } else if (queryString == u"roster") {
                uri.d->query = parseRosterQuery(urlQuery);
            } else if (queryString == u"subscribe") {
                uri.d->query = Subscribe();
            } else if (queryString == u"trust-message") {
                uri.d->query = parseTrustMessageQuery(urlQuery);
            } else if (queryString == u"unregister") {
                uri.d->query = Unregister();
            } else if (queryString == u"unsubscribe") {
                uri.d->query = Unsubscribe();
            } else {
                uri.d->query = parseCustomQuery(urlQuery);
            }
        }
    }

    return uri;
}

template<typename T>
bool serialize(const std::any &query, QUrlQuery &urlQuery)
{
    if (query.type() == typeid(T)) {
        serializeUrlQuery(std::any_cast<T>(query), urlQuery);
        return true;
    }
    return false;
}

/*! Serializes the URI to a string. */
QString QXmppUri::toString()
{
    QUrl url;
    url.setScheme(staticString(SCHEME));
    url.setPath(d->jid);

    // add URI query
    QUrlQuery urlQuery;
    urlQuery.setQueryDelimiters(QUERY_ITEM_KEY_DELIMITER, QUERY_ITEM_DELIMITER);

    if (d->query.has_value()) {
        serialize<Command>(d->query, urlQuery) ||
            serialize<Invite>(d->query, urlQuery) ||
            serialize<Join>(d->query, urlQuery) ||
            serialize<Login>(d->query, urlQuery) ||
            serialize<Message>(d->query, urlQuery) ||
            serialize<Unregister>(d->query, urlQuery) ||
            serialize<Unsubscribe>(d->query, urlQuery) ||
            serialize<Register>(d->query, urlQuery) ||
            serialize<Remove>(d->query, urlQuery) ||
            serialize<Roster>(d->query, urlQuery) ||
            serialize<Subscribe>(d->query, urlQuery) ||
            serialize<TrustMessage>(d->query, urlQuery) ||
            serialize<CustomQuery>(d->query, urlQuery);
    }

    url.setQuery(urlQuery);

    return QString::fromUtf8(url.toEncoded(QUrl::FullyEncoded));
}

/*!
    Returns the JID this URI is about.

    This can also be e.g. a MUC room in case of a Join action.
*/
QString QXmppUri::jid() const
{
    return d->jid;
}

/*!
    Sets the JID this URI links to.

    \a jid.
*/
void QXmppUri::setJid(const QString &jid)
{
    d->jid = jid;
}

/*!
    Returns the query of the URI.

    It may be empty (has_value() returns false). Possible URI types are available in the namespace
    QXmpp::Uri.
*/
std::any QXmppUri::query() const
{
    return d->query;
}

void QXmppUri::setQuery(std::any &&query)
{
    d->query = std::move(query);
}
