/*
 * Copyright (C) 2009  Barracuda Networks, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include "jinglertptasks.h"

#include "xmpp_xmlcommon.h"

static QDomElement candidateToElement(QDomDocument *doc, const XMPP::Ice176::Candidate &c)
{
    QDomElement e = doc->createElement("candidate");
    e.setAttribute("component", QString::number(c.component));
    e.setAttribute("foundation", c.foundation);
    e.setAttribute("generation", QString::number(c.generation));
    if (!c.id.isEmpty())
        e.setAttribute("id", c.id);
    e.setAttribute("ip", c.ip.toString());
    if (c.network != -1)
        e.setAttribute("network", QString::number(c.network));
    else // weird?
        e.setAttribute("network", QString::number(0));
    e.setAttribute("port", QString::number(c.port));
    e.setAttribute("priority", QString::number(c.priority));
    e.setAttribute("protocol", c.protocol);
    if (!c.rel_addr.isNull())
        e.setAttribute("rel-addr", c.rel_addr.toString());
    if (c.rel_port != -1)
        e.setAttribute("rel-port", QString::number(c.rel_port));
    // TODO: remove this?
    // if(!c.rem_addr.isNull())
    //    e.setAttribute("rem-addr", c.rem_addr.toString());
    // if(c.rem_port != -1)
    //    e.setAttribute("rem-port", QString::number(c.rem_port));
    e.setAttribute("type", c.type);
    return e;
}

static XMPP::Ice176::Candidate elementToCandidate(const QDomElement &e)
{
    if (e.tagName() != "candidate")
        return XMPP::Ice176::Candidate();

    XMPP::Ice176::Candidate c;
    c.component  = e.attribute("component").toInt();
    c.foundation = e.attribute("foundation");
    c.generation = e.attribute("generation").toInt();
    c.id         = e.attribute("id");
    c.ip         = QHostAddress(e.attribute("ip"));
    c.network    = e.attribute("network").toInt();
    c.port       = e.attribute("port").toInt();
    c.priority   = e.attribute("priority").toInt();
    c.protocol   = e.attribute("protocol");
    c.rel_addr   = QHostAddress(e.attribute("rel-addr"));
    c.rel_port   = e.attribute("rel-port").toInt();
    // TODO: remove this?
    // c.rem_addr = QHostAddress(e.attribute("rem-addr"));
    // c.rem_port = e.attribute("rem-port").toInt();
    c.type = e.attribute("type");
    return c;
}

static QDomElement payloadTypeToElement(QDomDocument *doc, const JingleRtpPayloadType &type)
{
    QDomElement e = doc->createElement("payload-type");
    e.setAttribute("id", QString::number(type.id));
    if (!type.name.isEmpty())
        e.setAttribute("name", type.name);
    e.setAttribute("clockrate", QString::number(type.clockrate));
    if (type.channels > 1)
        e.setAttribute("channels", QString::number(type.channels));
    if (type.ptime != -1)
        e.setAttribute("ptime", QString::number(type.ptime));
    if (type.maxptime != -1)
        e.setAttribute("maxptime", QString::number(type.maxptime));
    for (const JingleRtpPayloadType::Parameter &p : type.parameters) {
        QDomElement pe = doc->createElement("parameter");
        pe.setAttribute("name", p.name);
        pe.setAttribute("value", p.value);
        e.appendChild(pe);
    }
    return e;
}

static JingleRtpPayloadType elementToPayloadType(const QDomElement &e)
{
    if (e.tagName() != "payload-type")
        return JingleRtpPayloadType();

    JingleRtpPayloadType out;
    bool                 ok;
    int                  x;

    x = e.attribute("id").toInt(&ok);
    if (!ok)
        return JingleRtpPayloadType();
    out.id = x;

    out.name = e.attribute("name");

    x = e.attribute("clockrate").toInt(&ok);
    if (!ok)
        return JingleRtpPayloadType();
    out.clockrate = x;

    x = e.attribute("channels").toInt(&ok);
    if (ok)
        out.channels = x;

    x = e.attribute("ptime").toInt(&ok);
    if (ok)
        out.ptime = x;

    x = e.attribute("maxptime").toInt(&ok);
    if (ok)
        out.maxptime = x;

    QList<JingleRtpPayloadType::Parameter> plist;
    for (QDomNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) {
        if (!n.isElement())
            continue;

        QDomElement pe = n.toElement();
        if (pe.tagName() == "parameter") {
            JingleRtpPayloadType::Parameter p;
            p.name  = pe.attribute("name");
            p.value = pe.attribute("value");
            plist += p;
        }
    }
    out.parameters = plist;

    return out;
}

struct JingleCondEntry {
    const char *str;
    int         cond;
};

static JingleCondEntry jingleCondTable[] = { { "alternative-session", JingleRtpReason::AlternativeSession },
                                             { "busy", JingleRtpReason::Busy },
                                             { "cancel", JingleRtpReason::Cancel },
                                             { "connectivity-error", JingleRtpReason::ConnectivityError },
                                             { "decline", JingleRtpReason::Decline },
                                             { "expired", JingleRtpReason::Expired },
                                             { "failed-application", JingleRtpReason::FailedApplication },
                                             { "failed-transport", JingleRtpReason::FailedTransport },
                                             { "general-error", JingleRtpReason::GeneralError },
                                             { "gone", JingleRtpReason::Gone },
                                             { "incompatible-parameters", JingleRtpReason::IncompatibleParameters },
                                             { "media-error", JingleRtpReason::MediaError },
                                             { "security-error", JingleRtpReason::SecurityError },
                                             { "success", JingleRtpReason::Success },
                                             { "timeout", JingleRtpReason::Timeout },
                                             { "unsupported-applications", JingleRtpReason::UnsupportedApplications },
                                             { "unsupported-transports", JingleRtpReason::UnsupportedTransports },
                                             { nullptr, 0 } };

static QString conditionToElementName(JingleRtpReason::Condition cond)
{
    for (int n = 0; jingleCondTable[n].str; ++n) {
        if (jingleCondTable[n].cond == cond)
            return QString::fromLatin1(jingleCondTable[n].str);
    }
    return QString();
}

static int elementNameToCondition(const QString &in)
{
    for (int n = 0; jingleCondTable[n].str; ++n) {
        if (QLatin1String(jingleCondTable[n].str) == in)
            return jingleCondTable[n].cond;
    }
    return -1;
}

static QDomElement reasonToElement(QDomDocument *doc, const JingleRtpReason &reason)
{
    QDomElement e = doc->createElement("reason");
    e.appendChild(doc->createElement(conditionToElementName(reason.condition)));
    if (!reason.text.isEmpty()) {
        QDomElement text = doc->createElement("text");
        text.appendChild(doc->createTextNode(reason.text));
        e.appendChild(text);
    }
    return e;
}

static QDomElement firstChildElement(const QDomElement &in)
{
    for (QDomNode n = in.firstChild(); !n.isNull(); n = n.nextSibling()) {
        if (!n.isElement())
            continue;

        return n.toElement();
    }
    return QDomElement();
}

static JingleRtpReason elementToReason(const QDomElement &e)
{
    if (e.tagName() != "reason")
        return JingleRtpReason();

    QDomElement condElement = firstChildElement(e);
    if (condElement.isNull())
        return JingleRtpReason();
    int x = elementNameToCondition(condElement.tagName());
    if (x == -1)
        return JingleRtpReason();

    JingleRtpReason out;
    out.condition    = JingleRtpReason::Condition(x);
    QDomElement text = e.firstChildElement("text");
    if (!text.isNull())
        out.text = tagContent(text);

    return out;
}

//----------------------------------------------------------------------------
// JT_JingleRtp
//----------------------------------------------------------------------------
JT_JingleRtp::JT_JingleRtp(XMPP::Task *parent) : XMPP::Task(parent) { }

JT_JingleRtp::~JT_JingleRtp() = default;

void JT_JingleRtp::request(const XMPP::Jid &to, const JingleRtpEnvelope &envelope)
{
    to_               = to;
    iq_               = createIQ(doc(), "set", to.full(), id());
    QDomElement query = doc()->createElementNS("urn:xmpp:jingle:1", "jingle");
    query.setAttribute("action", envelope.action);
    if (!envelope.initiator.isEmpty())
        query.setAttribute("initiator", envelope.initiator);
    if (!envelope.responder.isEmpty())
        query.setAttribute("responder", envelope.responder);
    query.setAttribute("sid", envelope.sid);

    if (envelope.action == "session-terminate") {
        // for session terminate, there is no content list, just
        //   a reason for termination
        query.appendChild(reasonToElement(doc(), envelope.reason));
    } else {
        for (const JingleRtpContent &c : envelope.contentList) {
            QDomElement content = doc()->createElement("content");
            content.setAttribute("creator", c.creator);
            if (!c.disposition.isEmpty())
                content.setAttribute("disposition", c.disposition);
            content.setAttribute("name", c.name);
            if (!c.senders.isEmpty())
                content.setAttribute("senders", c.senders);

            if (!c.desc.media.isEmpty()) {
                // TODO: ssrc, bitrate, crypto
                QDomElement description = doc()->createElementNS("urn:xmpp:jingle:apps:rtp:1", "description");
                description.setAttribute("media", c.desc.media);
                for (const JingleRtpPayloadType &pt : c.desc.payloadTypes) {
                    QDomElement p = payloadTypeToElement(doc(), pt);
                    if (!p.isNull())
                        description.appendChild(p);
                }
                content.appendChild(description);
            }

            if (!c.trans.user.isEmpty()) {
                auto ns = c.trans.transportType == JingleRtpTrans::IceUdp ? "urn:xmpp:jingle:transports:ice-udp:1"
                                                                          : "urn:xmpp:jingle:transports:ice:0";
                QDomElement transport = doc()->createElementNS(ns, "transport");
                transport.setAttribute("ufrag", c.trans.user);
                transport.setAttribute("pwd", c.trans.pass);
                transport.setAttribute("ice2", QLatin1String("true"));
                for (const XMPP::Ice176::Candidate &ic : c.trans.candidates) {
                    QDomElement e = candidateToElement(doc(), ic);
                    if (!e.isNull())
                        transport.appendChild(e);
                }
                content.appendChild(transport);
                if (c.trans.gatheringComplete)
                    transport.appendChild(doc()->createElementNS(ns, "gathering-complete"));
            }

            query.appendChild(content);
        }
    }

    iq_.appendChild(query);
}

void JT_JingleRtp::onGo() { send(iq_); }

bool JT_JingleRtp::take(const QDomElement &x)
{
    if (!iqVerify(x, to_, id()))
        return false;

    if (x.attribute("type") == "result")
        setSuccess();
    else
        setError(x);

    return true;
}

//----------------------------------------------------------------------------
// JT_PushJingleRtp
//----------------------------------------------------------------------------
JT_PushJingleRtp::JT_PushJingleRtp(XMPP::Task *parent) : XMPP::Task(parent) { }

JT_PushJingleRtp::~JT_PushJingleRtp() = default;

void JT_PushJingleRtp::respondSuccess(const XMPP::Jid &to, const QString &id)
{
    QDomElement iq = createIQ(doc(), "result", to.full(), id);
    send(iq);
}

void JT_PushJingleRtp::respondError(const XMPP::Jid &to, const QString &id, int code, const QString &str)
{
    QDomElement iq  = createIQ(doc(), "error", to.full(), id);
    QDomElement err = textTag(doc(), "error", str);
    err.setAttribute("code", QString::number(code));
    iq.appendChild(err);
    send(iq);
}

bool JT_PushJingleRtp::take(const QDomElement &e)
{
    // must be an iq-set tag
    if (e.tagName() != "iq")
        return false;
    if (e.attribute("type") != "set")
        return false;

    QDomElement je;
    for (QDomNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) {
        if (!n.isElement())
            continue;

        QDomElement e = n.toElement();
        if (e.tagName() == "jingle" && e.namespaceURI() == "urn:xmpp:jingle:1") {
            je = e;
            break;
        }
    }

    if (je.isNull())
        return false;

    XMPP::Jid from  = e.attribute("from");
    QString   iq_id = e.attribute("id");

    JingleRtpEnvelope envelope;
    envelope.action    = je.attribute("action");
    envelope.initiator = je.attribute("initiator");
    envelope.responder = je.attribute("responder");
    envelope.sid       = je.attribute("sid");

    if (envelope.action == "session-terminate") {
        QDomElement re = je.firstChildElement("reason");
        if (re.isNull()) {
            respondError(from, iq_id, 400, QString());
            return true;
        }

        envelope.reason = elementToReason(re);
        if (int(envelope.reason.condition) == -1) {
            respondError(from, iq_id, 400, QString());
            return true;
        }
    } else {
        for (QDomNode n = je.firstChild(); !n.isNull(); n = n.nextSibling()) {
            if (!n.isElement())
                continue;

            QDomElement e = n.toElement();
            if (e.tagName() != "content")
                continue;

            JingleRtpContent c;
            c.creator     = e.attribute("creator");
            c.disposition = e.attribute("disposition");
            c.name        = e.attribute("name");
            c.senders     = e.attribute("senders");

            for (QDomNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) {
                if (!n.isElement())
                    continue;

                QDomElement e = n.toElement();
                if (e.tagName() == "description" && e.namespaceURI() == "urn:xmpp:jingle:apps:rtp:1") {
                    c.desc.media = e.attribute("media");

                    for (QDomNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) {
                        if (!n.isElement())
                            continue;

                        QDomElement          e  = n.toElement();
                        JingleRtpPayloadType pt = elementToPayloadType(e);
                        if (pt.id == -1) {
                            respondError(from, iq_id, 400, QString());
                            return true;
                        }

                        c.desc.payloadTypes += pt;
                    }
                } else if (e.tagName() == "transport" && e.namespaceURI() == "urn:xmpp:jingle:transports:ice-udp:1") {
                    c.trans.user          = e.attribute("ufrag");
                    c.trans.pass          = e.attribute("pwd");
                    c.trans.transportType = JingleRtpTrans::IceUdp;

                    for (QDomNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) {
                        if (!n.isElement())
                            continue;

                        QDomElement             e  = n.toElement();
                        XMPP::Ice176::Candidate ic = elementToCandidate(e);
                        if (ic.type.isEmpty()) {
                            respondError(from, iq_id, 400, QString());
                            return true;
                        }

                        c.trans.candidates += ic;
                    }
                } else if (e.tagName() == "transport" && e.namespaceURI() == "urn:xmpp:jingle:transports:ice:0") {
                    c.trans.user          = e.attribute("ufrag");
                    c.trans.pass          = e.attribute("pwd");
                    c.trans.ice2          = e.attribute("ice2") == QLatin1String("true");
                    c.trans.transportType = JingleRtpTrans::Ice;

                    for (auto n = e.firstChildElement(); !n.isNull(); n = n.nextSiblingElement()) {
                        if (n.tagName() == QLatin1String("gathering-complete")) {
                            c.trans.gatheringComplete = true;
                            continue;
                        }
                        if (n.tagName() != QLatin1String("candidate")) {
                            respondError(from, iq_id, 501, n.nodeName());
                            return true;
                        }

                        XMPP::Ice176::Candidate ic = elementToCandidate(n);
                        if (ic.type.isEmpty()) {
                            respondError(from, iq_id, 400, QString());
                            return true;
                        }

                        c.trans.candidates += ic;
                    }
                }
            }

            envelope.contentList += c;
        }
    }

    emit incomingRequest(from, iq_id, envelope);
    return true;
}
