/*
 * Copyright (C) 2009  Barracuda Networks, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 *
 */

#include "stunbinding.h"

#include <QHostAddress>
#include "stunmessage.h"
#include "stuntypes.h"
#include "stuntransaction.h"

namespace XMPP {

class StunBinding::Private : public QObject
{
    Q_OBJECT

public:
    StunBinding *q;
    StunTransactionPool *pool;
    StunTransaction *trans;
    QHostAddress stunAddr;
    int stunPort;
    QHostAddress addr;
    int port;
    QString errorString;
    bool use_extPriority, use_extIceControlling, use_extIceControlled;
    quint32 extPriority;
    bool extUseCandidate;
    quint64 extIceControlling, extIceControlled;
    QString stuser, stpass;
    bool fpRequired;

    Private(StunBinding *_q) :
        QObject(_q),
        q(_q),
        pool(0),
        trans(0),
        use_extPriority(false),
        use_extIceControlling(false),
        use_extIceControlled(false),
        extUseCandidate(false),
        fpRequired(false)
    {
    }

    ~Private()
    {
        delete trans;
    }

    void start(const QHostAddress &_addr = QHostAddress(), int _port = -1)
    {
        Q_ASSERT(!trans);

        stunAddr = _addr;
        stunPort = _port;

        trans = new StunTransaction(this);
        connect(trans, SIGNAL(createMessage(QByteArray)), SLOT(trans_createMessage(QByteArray)));
        connect(trans, SIGNAL(finished(XMPP::StunMessage)), SLOT(trans_finished(XMPP::StunMessage)));
        connect(trans, SIGNAL(error(XMPP::StunTransaction::Error)), SLOT(trans_error(XMPP::StunTransaction::Error)));

        if(!stuser.isEmpty())
        {
            trans->setShortTermUsername(stuser);
            trans->setShortTermPassword(stpass);
        }

        trans->setFingerprintRequired(fpRequired);

        trans->start(pool, stunAddr, stunPort);
    }

private slots:
    void trans_createMessage(const QByteArray &transactionId)
    {
        StunMessage message;
        message.setMethod(StunTypes::Binding);
        message.setId((const quint8 *)transactionId.data());

        QList<StunMessage::Attribute> list;

        if(use_extPriority)
        {
            StunMessage::Attribute a;
            a.type = StunTypes::PRIORITY;
            a.value = StunTypes::createPriority(extPriority);
            list += a;
        }

        if(extUseCandidate)
        {
            StunMessage::Attribute a;
            a.type = StunTypes::USE_CANDIDATE;
            list += a;
        }

        if(use_extIceControlling)
        {
            StunMessage::Attribute a;
            a.type = StunTypes::ICE_CONTROLLING;
            a.value = StunTypes::createIceControlling(extIceControlling);
            list += a;
        }

        if(use_extIceControlled)
        {
            StunMessage::Attribute a;
            a.type = StunTypes::ICE_CONTROLLED;
            a.value = StunTypes::createIceControlled(extIceControlled);
            list += a;
        }

        message.setAttributes(list);

        trans->setMessage(message);
    }

    void trans_finished(const XMPP::StunMessage &response)
    {
        delete trans;
        trans = 0;

        bool error = false;
        int code;
        QString reason;
        if(response.mclass() == StunMessage::ErrorResponse)
        {
            if(!StunTypes::parseErrorCode(response.attribute(StunTypes::ERROR_CODE), &code, &reason))
            {
                errorString = "Unable to parse ERROR-CODE in error response.";
                emit q->error(StunBinding::ErrorProtocol);
                return;
            }

            error = true;
        }

        if(error)
        {
            errorString = reason;
            if(code == StunTypes::RoleConflict)
                emit q->error(StunBinding::ErrorConflict);
            else
                emit q->error(StunBinding::ErrorRejected);
            return;
        }

        QHostAddress saddr;
        quint16 sport = 0;

        QByteArray val;
        val = response.attribute(StunTypes::XOR_MAPPED_ADDRESS);
        if(!val.isNull())
        {
            if(!StunTypes::parseXorMappedAddress(val, response.magic(), response.id(), &saddr, &sport))
            {
                errorString = "Unable to parse XOR-MAPPED-ADDRESS response.";
                emit q->error(StunBinding::ErrorProtocol);
                return;
            }
        }
        else
        {
            val = response.attribute(StunTypes::MAPPED_ADDRESS);
            if(!val.isNull())
            {
                if(!StunTypes::parseMappedAddress(val, &saddr, &sport))
                {
                    errorString = "Unable to parse MAPPED-ADDRESS response.";
                    emit q->error(StunBinding::ErrorProtocol);
                    return;
                }
            }
            else
            {
                errorString = "Response does not contain XOR-MAPPED-ADDRESS or MAPPED-ADDRESS.";
                emit q->error(StunBinding::ErrorProtocol);
                return;
            }
        }

        addr = saddr;
        port = sport;
        emit q->success();
    }

    void trans_error(XMPP::StunTransaction::Error e)
    {
        delete trans;
        trans = 0;

        if(e == StunTransaction::ErrorTimeout)
        {
            errorString = "Request timed out.";
            emit q->error(StunBinding::ErrorTimeout);
        }
        else
        {
            errorString = "Generic transaction error.";
            emit q->error(StunBinding::ErrorGeneric);
        }
    }
};

StunBinding::StunBinding(StunTransactionPool *pool) :
    QObject(pool)
{
    d = new Private(this);
    d->pool = pool;
}

StunBinding::~StunBinding()
{
    delete d;
}

void StunBinding::setPriority(quint32 i)
{
    d->use_extPriority = true;
    d->extPriority = i;
}

void StunBinding::setUseCandidate(bool enabled)
{
    d->extUseCandidate = enabled;
}

void StunBinding::setIceControlling(quint64 i)
{
    d->use_extIceControlling = true;
    d->extIceControlling = i;
}

void StunBinding::setIceControlled(quint64 i)
{
    d->use_extIceControlled = true;
    d->extIceControlled = i;
}

void StunBinding::setShortTermUsername(const QString &username)
{
    d->stuser = username;
}

void StunBinding::setShortTermPassword(const QString &password)
{
    d->stpass = password;
}

void StunBinding::setFingerprintRequired(bool enabled)
{
    d->fpRequired = enabled;
}

void StunBinding::start()
{
    d->start();
}

void StunBinding::start(const QHostAddress &addr, int port)
{
    d->start(addr, port);
}

QHostAddress StunBinding::reflexiveAddress() const
{
    return d->addr;
}

int StunBinding::reflexivePort() const
{
    return d->port;
}

QString StunBinding::errorString() const
{
    return d->errorString;
}

}

#include "stunbinding.moc"
