/*
 * Copyright (C) 2005-2008  Justin Karneges
 *
 * 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 WITHOUT ANY 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 "irisnetplugin.h"

#include "objectsession.h"
#include "qjdnsshared.h"
#include "netinterface.h"

//#define JDNS_DEBUG

Q_DECLARE_METATYPE(XMPP::NameRecord)
//Q_DECLARE_METATYPE(XMPP::NameResolver::Error)
Q_DECLARE_METATYPE(XMPP::ServiceBrowser::Error)
Q_DECLARE_METATYPE(XMPP::ServiceResolver::Error)
Q_DECLARE_METATYPE(XMPP::ServiceLocalPublisher::Error)

namespace XMPP {

static NameRecord importJDNSRecord(const QJDns::Record &in)
{
    NameRecord out;
    switch(in.type)
    {
        case QJDns::A:     out.setAddress(in.address); break;
        case QJDns::Aaaa:  out.setAddress(in.address); break;
        case QJDns::Mx:    out.setMx(in.name, in.priority); break;
        case QJDns::Srv:   out.setSrv(in.name, in.port, in.priority, in.weight); break;
        case QJDns::Cname: out.setCname(in.name); break;
        case QJDns::Ptr:   out.setPtr(in.name); break;
        case QJDns::Txt:   out.setTxt(in.texts); break;
        case QJDns::Hinfo: out.setHinfo(in.cpu, in.os); break;
        case QJDns::Ns:    out.setNs(in.name); break;
        case 10:           out.setNull(in.rdata); break;
        default:
            return out;
    }
    out.setOwner(in.owner);
    out.setTtl(in.ttl);
    return out;
}

static QJDns::Record exportJDNSRecord(const NameRecord &in)
{
    QJDns::Record out;
    switch(in.type())
    {
        case NameRecord::A:
            out.type = QJDns::A;
            out.haveKnown = true;
            out.address = in.address();
            break;
        case NameRecord::Aaaa:
            out.type = QJDns::Aaaa;
            out.haveKnown = true;
            out.address = in.address();
            break;
        case NameRecord::Mx:
            out.type = QJDns::Mx;
            out.haveKnown = true;
            out.name = in.name();
            out.priority = in.priority();
            break;
        case NameRecord::Srv:
            out.type = QJDns::Srv;
            out.haveKnown = true;
            out.name = in.name();
            out.port = in.port();
            out.priority = in.priority();
            out.weight = in.weight();
            break;
        case NameRecord::Cname:
            out.type = QJDns::Cname;
            out.haveKnown = true;
            out.name = in.name();
            break;
        case NameRecord::Ptr:
            out.type = QJDns::Ptr;
            out.haveKnown = true;
            out.name = in.name();
            break;
        case NameRecord::Txt:
            out.type = QJDns::Txt;
            out.haveKnown = true;
            out.texts = in.texts();
            break;
        case NameRecord::Hinfo:
            out.type = QJDns::Hinfo;
            out.haveKnown = true;
            out.cpu = in.cpu();
            out.os = in.os();
            break;
        case NameRecord::Ns:
            out.type = QJDns::Ns;
            out.haveKnown = true;
            out.name = in.name();
            break;
        case NameRecord::Null:
            out.type = 10;
            out.rdata = in.rawData();
            break;
        default:
            return out;
    }
    out.owner = in.owner();
    out.ttl = in.ttl();
    return out;
}

static bool validServiceType(const QByteArray &in)
{
    // can't be empty, or start/end with a dot
    if(in.isEmpty() || in[0] == '.' || in[in.length() - 1] == '.')
        return false;

    // must contain exactly one dot
    int dotcount = 0;
    for(int n = 0; n < in.length(); ++n)
    {
        if(in[n] == '.')
        {
            ++dotcount;

            // no need to count more than 2
            if(dotcount >= 2)
                break;
        }
    }
    if(dotcount != 1)
        return false;

    return true;
}

static QByteArray escapeDomainPart(const QByteArray &in)
{
    QByteArray out;
    for(int n = 0; n < in.length(); ++n)
    {
        if(in[n] == '\\')
            out += "\\\\";
        else if(in[n] == '.')
            out += "\\.";
        else
            out += in[n];
    }
    return out;
}

static QByteArray unescapeDomainPart(const QByteArray &in)
{
    QByteArray out;
    for(int n = 0; n < in.length(); ++n)
    {
        if(in[n] == '\\')
        {
            if(n + 1 >= in.length())
                return QByteArray();

            out += in[n + 1];
        }
        else
            out += in[n];
    }
    return out;
}

class IdManager
{
private:
    QSet<int> set;
    int at;

    inline void bump_at()
    {
        if(at == 0x7fffffff)
            at = 0;
        else
            ++at;
    }

public:
    IdManager() :
        at(0)
    {
    }

    int reserveId()
    {
        while(1)
        {
            if(!set.contains(at))
            {
                int id = at;
                set.insert(id);
                bump_at();
                return id;
            }

            bump_at();
        }
    }

    void releaseId(int id)
    {
        set.remove(id);
    }

    void clear()
    {
        set.clear();
        at = 0;
    }
};

//----------------------------------------------------------------------------
// JDnsGlobal
//----------------------------------------------------------------------------
class JDnsGlobal : public QObject
{
    Q_OBJECT

public:
    QJDnsSharedDebug db;
    QJDnsShared *uni_net, *uni_local, *mul;
    QHostAddress mul_addr4, mul_addr6;
    NetInterfaceManager netman;
    QList<NetInterface*> ifaces;
    QTimer *updateTimer;

    JDnsGlobal()
    {
        uni_net = 0;
        uni_local = 0;
        mul = 0;

        qRegisterMetaType<NameRecord>();
        qRegisterMetaType<NameResolver::Error>();
        qRegisterMetaType<ServiceBrowser::Error>();
        qRegisterMetaType<ServiceResolver::Error>();
        qRegisterMetaType<ServiceLocalPublisher::Error>();

        connect(&db, SIGNAL(readyRead()), SLOT(jdns_debugReady()));

        updateTimer = new QTimer(this);
        connect(updateTimer, SIGNAL(timeout()), SLOT(doUpdateMulticastInterfaces()));
        updateTimer->setSingleShot(true);
    }

    ~JDnsGlobal()
    {
        updateTimer->disconnect(this);
        updateTimer->setParent(0);
        updateTimer->deleteLater();

        qDeleteAll(ifaces);

        QList<QJDnsShared*> list;
        if(uni_net)
            list += uni_net;
        if(uni_local)
            list += uni_local;
        if(mul)
            list += mul;

        // calls shutdown on the list, waits for shutdownFinished, deletes
        QJDnsShared::waitForShutdown(list);

        // get final debug
        jdns_debugReady();
    }

    QJDnsShared *ensure_uni_net()
    {
        if(!uni_net)
        {
            uni_net = new QJDnsShared(QJDnsShared::UnicastInternet, this);
            uni_net->setDebug(&db, "U");
            bool ok4 = uni_net->addInterface(QHostAddress::Any);
            bool ok6 = uni_net->addInterface(QHostAddress::AnyIPv6);
            if(!ok4 && !ok6)
            {
                delete uni_net;
                uni_net = 0;
            }
        }
        return uni_net;
    }

    QJDnsShared *ensure_uni_local()
    {
        if(!uni_local)
        {
            uni_local = new QJDnsShared(QJDnsShared::UnicastLocal, this);
            uni_local->setDebug(&db, "L");
            bool ok4 = uni_local->addInterface(QHostAddress::Any);
            bool ok6 = uni_local->addInterface(QHostAddress::AnyIPv6);
            if(!ok4 && !ok6)
            {
                delete uni_local;
                uni_local = 0;
            }
        }
        return uni_local;
    }

    QJDnsShared *ensure_mul()
    {
        if(!mul)
        {
            mul = new QJDnsShared(QJDnsShared::Multicast, this);
            mul->setDebug(&db, "M");

            connect(&netman, SIGNAL(interfaceAvailable(QString)), SLOT(iface_available(QString)));

            // get the current network interfaces.  this initial
            //   fetching should not trigger any calls to
            //   updateMulticastInterfaces().  only future
            //   activity should do that.
            foreach(const QString &id, netman.interfaces())
            {
                NetInterface *iface = new NetInterface(id, &netman);
                connect(iface, SIGNAL(unavailable()), SLOT(iface_unavailable()));
                ifaces += iface;
            }

            updateMulticastInterfaces(false);
        }
        return mul;
    }

    bool haveMulticast4() const
    {
        return !mul_addr4.isNull();
    }

    bool haveMulticast6() const
    {
        return !mul_addr6.isNull();
    }

signals:
    void interfacesChanged();

private slots:
    void jdns_debugReady()
    {
        QStringList lines = db.readDebugLines();
#ifdef JDNS_DEBUG
        for(int n = 0; n < lines.count(); ++n)
            qDebug("jdns: %s\n", qPrintable(lines[n]));
#else
        Q_UNUSED(lines);
#endif
    }

    void iface_available(const QString &id)
    {
        NetInterface *iface = new NetInterface(id, &netman);
        connect(iface, SIGNAL(unavailable()), SLOT(iface_unavailable()));
        ifaces += iface;

        updateTimer->start(100);
    }

    void iface_unavailable()
    {
        NetInterface *iface = static_cast<NetInterface *>(sender());
        ifaces.removeAll(iface);
        delete iface;

        updateTimer->start(100);
    }

    void doUpdateMulticastInterfaces()
    {
        updateMulticastInterfaces(true);
    }

private:
    void updateMulticastInterfaces(bool useSignals)
    {
        QHostAddress addr4 = QJDns::detectPrimaryMulticast(QHostAddress::Any);
        QHostAddress addr6 = QJDns::detectPrimaryMulticast(QHostAddress::AnyIPv6);

        bool had4 = !mul_addr4.isNull();
        bool had6 = !mul_addr6.isNull();

        updateMulticastInterface(&mul_addr4, addr4);
        updateMulticastInterface(&mul_addr6, addr6);

        bool have4 = !mul_addr4.isNull();
        bool have6 = !mul_addr6.isNull();

        // did we gain/lose something?
        if(had4 != have4 || had6 != have6)
        {
            if(useSignals)
                emit interfacesChanged();
        }
    }

    void updateMulticastInterface(QHostAddress *curaddr, const QHostAddress &newaddr)
    {
        if(!(newaddr == *curaddr)) // QHostAddress doesn't have operator!=
        {
            if(!curaddr->isNull())
                mul->removeInterface(*curaddr);
            *curaddr = newaddr;
            if(!curaddr->isNull())
            {
                if(!mul->addInterface(*curaddr))
                    *curaddr = QHostAddress();
            }
        }
    }
};

//----------------------------------------------------------------------------
// JDnsNameProvider
//----------------------------------------------------------------------------
class JDnsNameProvider : public NameProvider
{
    Q_OBJECT
    Q_INTERFACES(XMPP::NameProvider)

public:
    enum Mode { Internet, Local };

    JDnsGlobal *global;
    Mode mode;
    IdManager idman;
    ObjectSession sess;

    class Item
    {
    public:
        int id;
        QJDnsSharedRequest *req;
        int type;
        bool longLived;
        ObjectSession sess;
        bool useLocal;
        bool localResult;
        NameResolver::Error error;
        NameResolver::Error localError;

        Item(QObject *parent = 0) :
            id(-1),
            req(0),
            sess(parent),
            useLocal(false),
            localResult(false)
        {
        }

        ~Item()
        {
            delete req;
        }
    };
    QList<Item*> items;

    static JDnsNameProvider *create(JDnsGlobal *global, Mode mode, QObject *parent = 0)
    {
        if(mode == Internet)
        {
            if(!global->ensure_uni_net())
                return 0;
        }
        else
        {
            if(!global->ensure_uni_local())
                return 0;
        }

        return new JDnsNameProvider(global, mode, parent);
    }

    JDnsNameProvider(JDnsGlobal *_global, Mode _mode, QObject *parent = 0) :
        NameProvider(parent)
    {
        global = _global;
        mode = _mode;
    }

    ~JDnsNameProvider()
    {
        qDeleteAll(items);
    }

    Item *getItemById(int id)
    {
        for(int n = 0; n < items.count(); ++n)
        {
            if(items[n]->id == id)
                return items[n];
        }

        return 0;
    }

    Item *getItemByReq(QJDnsSharedRequest *req)
    {
        for(int n = 0; n < items.count(); ++n)
        {
            if(items[n]->req == req)
                return items[n];
        }

        return 0;
    }

    void releaseItem(Item *i)
    {
        idman.releaseId(i->id);
        items.removeAll(i);
        delete i;
    }

    void tryError(Item *i)
    {
        // if we are doing dual resolves, make sure both are done
        if(!i->longLived && (i->req || (i->useLocal && !i->localResult)))
            return;

        int id = i->id;
        NameResolver::Error error = i->error;
        releaseItem(i);
        emit resolve_error(id, error);
    }

    virtual bool supportsSingle() const
    {
        return true;
    }

    virtual bool supportsLongLived() const
    {
        if(mode == Local)
            return true;  // we support long-lived local queries
        else
            return false; // we do NOT support long-lived internet queries
    }

    virtual bool supportsRecordType(int type) const
    {
        // all record types supported
        Q_UNUSED(type);
        return true;
    }

    virtual int resolve_start(const QByteArray &name, int qType, bool longLived)
    {
        if(mode == Internet)
        {
            bool isLocalName = false;
            if(name.right(6) == ".local" || name.right(7) == ".local.")
                isLocalName = true;

            // if query ends in .local, switch to local resolver
            /*if(isLocalName)
            {
                Item *i = new Item(this);
                i->id = idman.reserveId();
                i->longLived = longLived;
                items += i;
                i->sess.defer(this, "do_local", Q_ARG(int, i->id), Q_ARG(QByteArray, name));
                return i->id;
            }*/

            // we don't support long-lived internet queries
            if(longLived)
            {
                // but we do support long-lived local queries
                if(isLocalName)
                {
                    Item *i = new Item(this);
                    i->id = idman.reserveId();
                    i->longLived = longLived;
                    i->useLocal = true;
                    items += i;
                    i->sess.defer(this, "do_local", Q_ARG(int, i->id), Q_ARG(QByteArray, name));
                    return i->id;
                }

                Item *i = new Item(this);
                i->id = idman.reserveId();
                items += i;
                i->sess.defer(this, "do_error", Q_ARG(int, i->id),
                    Q_ARG(XMPP::NameResolver::Error, NameResolver::ErrorNoLongLived));
                return i->id;
            }

            // perform the query
            Item *i = new Item(this);
            i->id = idman.reserveId();
            i->req = new QJDnsSharedRequest(global->uni_net);
            connect(i->req, SIGNAL(resultsReady()), SLOT(req_resultsReady()));
            i->type = qType;
            i->longLived = false;
            if(isLocalName)
                i->useLocal = true;
            items += i;
            i->req->query(name, qType);
            // if query ends in .local, simultaneously do local resolve
            if(isLocalName)
                i->sess.defer(this, "do_local", Q_ARG(int, i->id), Q_ARG(QByteArray, name));
            return i->id;
        }
        else
        {
            Item *i = new Item(this);
            i->id = idman.reserveId();
            i->type = qType;
            if(longLived)
            {
                if(!global->ensure_mul())
                {
                    items += i;
                    i->sess.defer(this, "do_error", Q_ARG(int, i->id),
                        Q_ARG(XMPP::NameResolver::Error, NameResolver::ErrorNoLocal));
                    return i->id;
                }

                i->req = new QJDnsSharedRequest(global->mul);
                i->longLived = true;
            }
            else
            {
                i->req = new QJDnsSharedRequest(global->uni_local);
                i->longLived = false;
            }
            connect(i->req, SIGNAL(resultsReady()), SLOT(req_resultsReady()));
            items += i;
            i->req->query(name, qType);
            return i->id;
        }
    }

    virtual void resolve_stop(int id)
    {
        Item *i = getItemById(id);
        Q_ASSERT(i);

        if(i->req)
            i->req->cancel();
        releaseItem(i);
    }

    virtual void resolve_localResultsReady(int id, const QList<XMPP::NameRecord> &results)
    {
        Item *i = getItemById(id);
        Q_ASSERT(i);
        Q_ASSERT(!i->localResult);

        i->localResult = true;
        i->sess.defer(this, "do_local_ready", Q_ARG(int, id),
            Q_ARG(QList<XMPP::NameRecord>, results));
    }

    virtual void resolve_localError(int id, XMPP::NameResolver::Error e)
    {
        Item *i = getItemById(id);
        Q_ASSERT(i);
        Q_ASSERT(!i->localResult);

        i->localResult = true;
        i->sess.defer(this, "do_local_error", Q_ARG(int, id),
            Q_ARG(XMPP::NameResolver::Error, e));
    }

private slots:
    void req_resultsReady()
    {
        QJDnsSharedRequest *req = static_cast<QJDnsSharedRequest *>(sender());
        Item *i = getItemByReq(req);
        Q_ASSERT(i);

        int id = i->id;

        NameResolver::Error error;

        if(req->success())
        {
            QList<NameRecord> out;
            foreach(const QJDns::Record &r, req->results())
            {
                // unless we are asking for all types, only
                //   accept the type we asked for
                if(i->type == QJDns::Any || r.type == i->type)
                {
                    NameRecord rec = importJDNSRecord(r);
                    if(!rec.isNull())
                        out += rec;
                }
            }

            // don't report anything if long-lived gives no results
            if(i->longLived && out.isEmpty())
                return;

            // only emit success if we have at least 1 result
            if(!out.isEmpty())
            {
                // FIXME: need a way to cancel related local
                //   query if still active

                if(!i->longLived)
                    releaseItem(i);
                emit resolve_resultsReady(id, out);
                return;
            }
            else
            {
                error = NameResolver::ErrorGeneric;
            }
        }
        else
        {
            QJDnsSharedRequest::Error e = req->error();

            error = NameResolver::ErrorGeneric;
            if(e == QJDnsSharedRequest::ErrorNXDomain)
                error = NameResolver::ErrorNoName;
            else if(e == QJDnsSharedRequest::ErrorTimeout)
                error = NameResolver::ErrorTimeout;
            else // ErrorGeneric or ErrorNoNet
                error = NameResolver::ErrorGeneric;
        }

        delete i->req;
        i->req = 0;
        i->error = error;
        tryError(i);
    }

    void do_error(int id, XMPP::NameResolver::Error e)
    {
        Item *i = getItemById(id);
        Q_ASSERT(i);

        // note: for an internet resolve, this slot is not called in
        //   any case where a local subquery is invoked as well (yet?)

        releaseItem(i);
        emit resolve_error(id, e);
    }

    void do_local(int id, const QByteArray &name)
    {
        //Item *i = getItemById(id);
        //Q_ASSERT(i);

        /*// resolve_useLocal has two behaviors:
        // - if longlived, then it indicates a hand-off
        // - if non-longlived, then it indicates we want a subquery

        if(i->longLived)
            releaseItem(i);*/
        emit resolve_useLocal(id, name);
    }

    void do_local_ready(int id, const QList<XMPP::NameRecord> &results)
    {
        Item *i = getItemById(id);
        Q_ASSERT(i);

        if(!i->longLived)
        {
            // stop any simultaneous internet resolve
            if(i->req)
                i->req->cancel();

            // for non-longlived, we're done
            releaseItem(i);
        }

        emit resolve_resultsReady(id, results);
    }

    void do_local_error(int id, XMPP::NameResolver::Error e)
    {
        Item *i = getItemById(id);
        Q_ASSERT(i);

        i->localError = e;
        tryError(i);
    }
};

//----------------------------------------------------------------------------
// JDnsBrowse
//----------------------------------------------------------------------------
class JDnsBrowse : public QObject
{
    Q_OBJECT

public:
    QByteArray type, typeAndDomain;
    QJDnsSharedRequest req;

    JDnsBrowse(QJDnsShared *_jdns, QObject *parent = 0) :
        QObject(parent),
        req(_jdns, this)
    {
        connect(&req, SIGNAL(resultsReady()), SLOT(jdns_resultsReady()));
    }

    void start(const QByteArray &_type)
    {
        type = _type;
        Q_ASSERT(validServiceType(type));
        typeAndDomain = type + ".local.";
        req.query(typeAndDomain, QJDns::Ptr);
    }

signals:
    void available(const QByteArray &instance);
    void unavailable(const QByteArray &instance);

private:
    QByteArray parseInstanceName(const QByteArray &name)
    {
        // needs to be at least X + '.' + typeAndDomain
        if(name.length() < typeAndDomain.length() + 2)
            return QByteArray();

        // index of the '.' character
        int at = name.length() - typeAndDomain.length() - 1;

        if(name[at] != '.')
            return QByteArray();
        if(name.mid(at + 1) != typeAndDomain)
            return QByteArray();

        QByteArray friendlyName = unescapeDomainPart(name.mid(0, at));
        if(friendlyName.isEmpty())
            return QByteArray();

        return friendlyName;
    }

private slots:
    void jdns_resultsReady()
    {
        // ignore errors
        if(!req.success())
            return;

        QJDns::Record rec = req.results().first();

        Q_ASSERT(rec.type == QJDns::Ptr);

        QByteArray name = rec.name;
        QByteArray instance = parseInstanceName(name);
        if(instance.isEmpty())
            return;

        if(rec.ttl == 0)
        {
            emit unavailable(instance);
            return;
        }

        emit available(instance);
    }
};

//----------------------------------------------------------------------------
// JDnsServiceResolve
//----------------------------------------------------------------------------

// 5 second timeout waiting for both A and AAAA
// 8 second timeout waiting for at least one record
class JDnsServiceResolve : public QObject
{
    Q_OBJECT

public:
    enum SrvState
    {
        Srv               = 0,
        AddressWait       = 1,
        AddressFirstCome  = 2
    };

    QJDnsSharedRequest reqtxt; // for TXT
    QJDnsSharedRequest req;    // for SRV/A
    QJDnsSharedRequest req6;   // for AAAA
    bool haveTxt;
    SrvState srvState;
    QTimer *opTimer;

    // out
    QList<QByteArray> attribs;
    QByteArray host;
    int port;
    bool have4, have6;
    QHostAddress addr4, addr6;

    JDnsServiceResolve(QJDnsShared *_jdns, QObject *parent = 0) :
        QObject(parent),
        reqtxt(_jdns, this),
        req(_jdns, this),
        req6(_jdns, this)
    {
        connect(&reqtxt, SIGNAL(resultsReady()), SLOT(reqtxt_ready()));
        connect(&req, SIGNAL(resultsReady()), SLOT(req_ready()));
        connect(&req6, SIGNAL(resultsReady()), SLOT(req6_ready()));

        opTimer = new QTimer(this);
        connect(opTimer, SIGNAL(timeout()), SLOT(op_timeout()));
        opTimer->setSingleShot(true);
    }

    ~JDnsServiceResolve()
    {
        opTimer->disconnect(this);
        opTimer->setParent(0);
        opTimer->deleteLater();
    }

    void start(const QByteArray name)
    {
        haveTxt = false;
        srvState = Srv;
        have4 = false;
        have6 = false;

        opTimer->start(8000);

        reqtxt.query(name, QJDns::Txt);
        req.query(name, QJDns::Srv);
    }

signals:
    void finished();
    void error(QJDnsSharedRequest::Error e);

private:
    void cleanup()
    {
        if(opTimer->isActive())
            opTimer->stop();
        if(!haveTxt)
            reqtxt.cancel();
        if(srvState == Srv || !have4)
            req.cancel();
        if(srvState >= AddressWait && !have6)
            req6.cancel();
    }

    bool tryDone()
    {
        // we're done when we have txt and addresses
        if(haveTxt && ( (have4 && have6) || (srvState == AddressFirstCome && (have4 || have6)) ))
        {
            cleanup();
            emit finished();
            return true;
        }

        return false;
    }

private slots:
    void reqtxt_ready()
    {
        if(!reqtxt.success())
        {
            cleanup();
            emit error(reqtxt.error());
            return;
        }

        QJDns::Record rec = reqtxt.results().first();
        reqtxt.cancel();

        Q_ASSERT(rec.type == QJDns::Txt);

        attribs.clear();
        if(!rec.texts.isEmpty())
        {
            // if there is only 1 text, it needs to be
            //   non-empty for us to care
            if(rec.texts.count() != 1 || !rec.texts[0].isEmpty())
                attribs = rec.texts;
        }

        haveTxt = true;

        tryDone();
    }

    void req_ready()
    {
        if(!req.success())
        {
            cleanup();
            emit error(req.error());
            return;
        }

        QJDns::Record rec = req.results().first();
        req.cancel();

        if(srvState == Srv)
        {
            // in Srv state, req is used for SRV records

            Q_ASSERT(rec.type == QJDns::Srv);

            host = rec.name;
            port = rec.port;

            srvState = AddressWait;
            opTimer->start(5000);

            req.query(host, QJDns::A);
            req6.query(host, QJDns::Aaaa);
        }
        else
        {
            // in the other states, req is used for A records

            Q_ASSERT(rec.type == QJDns::A);

            addr4 = rec.address;
            have4 = true;

            tryDone();
        }
    }

    void req6_ready()
    {
        if(!req6.success())
        {
            cleanup();
            emit error(req6.error());
            return;
        }

        QJDns::Record rec = req6.results().first();
        req6.cancel();

        Q_ASSERT(rec.type == QJDns::Aaaa);

        addr6 = rec.address;
        have6 = true;

        tryDone();
    }

    void op_timeout()
    {
        if(srvState == Srv)
        {
            // timeout getting SRV.  it is possible that we could
            //   have obtained the TXT record, but if SRV times
            //   out then we consider the whole job to have
            //   failed.
            cleanup();
            emit error(QJDnsSharedRequest::ErrorTimeout);
        }
        else if(srvState == AddressWait)
        {
            // timeout while waiting for both A and AAAA.  we now
            //   switch to the AddressFirstCome state, where an
            //   answer for either will do

            srvState = AddressFirstCome;

            // if we have at least one of these, we're done
            if(have4 || have6)
            {
                // well, almost.  we might still be waiting
                //   for the TXT record
                if(tryDone())
                    return;
            }

            // if we are here, then it means we are missing TXT
            //   still, or we have neither A nor AAAA.

            // wait 3 more seconds
            opTimer->start(3000);
        }
        else // AddressFirstCome
        {
            // last chance!
            if(!tryDone())
            {
                cleanup();
                emit error(QJDnsSharedRequest::ErrorTimeout);
            }
        }
    }
};

//----------------------------------------------------------------------------
// JDnsPublishAddresses
//----------------------------------------------------------------------------

// helper class for JDnsPublishAddresses.  publishes A+PTR or AAAA+PTR pair.
class JDnsPublishAddress : public QObject
{
    Q_OBJECT

public:
    enum Type
    {
        IPv4,
        IPv6
    };

    Type type;
    QByteArray host;
    QJDnsSharedRequest pub_addr;
    QJDnsSharedRequest pub_ptr;
    bool success_;

    JDnsPublishAddress(QJDnsShared *_jdns, QObject *parent = 0) :
        QObject(parent),
        pub_addr(_jdns, this),
        pub_ptr(_jdns, this)
    {
        connect(&pub_addr, SIGNAL(resultsReady()), SLOT(pub_addr_ready()));
        connect(&pub_ptr, SIGNAL(resultsReady()), SLOT(pub_ptr_ready()));
    }

    void start(Type _type, const QByteArray &_host)
    {
        type = _type;
        host = _host;
        success_ = false;

        QJDns::Record rec;
        if(type == IPv6)
            rec.type = QJDns::Aaaa;
        else
            rec.type = QJDns::A;
        rec.owner = host;
        rec.ttl = 120;
        rec.haveKnown = true;
        rec.address = QHostAddress(); // null address, will be filled in
        pub_addr.publish(QJDns::Unique, rec);
    }

    void cancel()
    {
        pub_addr.cancel();
        pub_ptr.cancel();
    }

    bool success() const
    {
        return success_;
    }

signals:
    void resultsReady();

private slots:
    void pub_addr_ready()
    {
        if(pub_addr.success())
        {
            QJDns::Record rec;
            rec.type = QJDns::Ptr;
            if(type == IPv6)
                rec.owner = ".ip6.arpa.";
            else
                rec.owner = ".in-addr.arpa.";
            rec.ttl = 120;
            rec.haveKnown = true;
            rec.name = host;
            pub_ptr.publish(QJDns::Shared, rec);
        }
        else
        {
            pub_ptr.cancel(); // needed if addr fails during or after ptr
            success_ = false;
            emit resultsReady();
        }
    }

    void pub_ptr_ready()
    {
        if(pub_ptr.success())
        {
            success_ = true;
        }
        else
        {
            pub_addr.cancel();
            success_ = false;
        }

        emit resultsReady();
    }
};

// This class publishes A/AAAA records for the machine, using a derived
//   hostname (it will use QHostInfo::localHostName(), but append a unique
//   suffix if necessary).  If there is ever a record conflict, it will
//   republish under a unique name.
//
// The hostName() signal is emitted when a hostname is successfully
//   published as.  When there is a conflict, hostName() is emitted with
//   an empty value, and will again be emitted with a non-empty value
//   once the conflict is resolved.  A missing hostname is considered a
//   temporary problem, and so other publish operations that depend on a
//   hostname (SRV, etc) should block until a hostname is available.
class JDnsPublishAddresses : public QObject
{
    Q_OBJECT

public:
    bool started;
    bool use6, use4;
    JDnsPublishAddress pub6;
    JDnsPublishAddress pub4;
    int counter;
    QByteArray host;
    bool success;
    bool have6, have4;
    ObjectSession sess;

    JDnsPublishAddresses(QJDnsShared *_jdns, QObject *parent = 0) :
        QObject(parent),
        started(false),
        use6(false),
        use4(false),
        pub6(_jdns, this),
        pub4(_jdns, this),
        sess(this)
    {
        connect(&pub6, SIGNAL(resultsReady()), SLOT(pub6_ready()));
        connect(&pub4, SIGNAL(resultsReady()), SLOT(pub4_ready()));
    }

    void start()
    {
        counter = 1;
        success = false;
        have6 = false;
        have4 = false;
        started = true;
        tryPublish();
    }

    bool isStarted() const
    {
        return started;
    }

    // comments in this method apply to setUseIPv4 as well.
    void setUseIPv6(bool b)
    {
        if(b == use6)
            return;

        use6 = b;
        if(!started)
            return;

        // a "deferred call to doDisable" and "publish operations"
        //   are mutually exclusive.  thus, a deferred call is only
        //   invoked when both publishes are canceled, and the
        //   deferred call is canceled if any of the publishes are
        //   reinstantiated.

        if(use6)
        {
            if(use4)
            {
                // if the other is already active, then
                //   just activate this one without
                //   recomputing the hostname
                tryPublish6();
            }
            else
            {
                sess.reset();

                // otherwise, recompute the hostname
                tryPublish();
            }
        }
        else
        {
            pub6.cancel();
            have6 = false;
            if(!use4)
                sess.defer(this, "doDisable");
        }
    }

    void setUseIPv4(bool b)
    {
        if(b == use4)
            return;

        use4 = b;
        if(!started)
            return;

        if(use4)
        {
            if(use6)
            {
                tryPublish4();
            }
            else
            {
                sess.reset();
                tryPublish();
            }
        }
        else
        {
            pub4.cancel();
            have4 = false;
            if(!use6)
                sess.defer(this, "doDisable");
        }
    }

signals:
    void hostName(const QByteArray &str);

private:
    void tryPublish()
    {
        QString me = QHostInfo::localHostName();

        // some hosts may already have ".local" in their name
        if(me.endsWith(".local"))
            me.truncate(me.length() - 6);

        // prefix our hostname so we don't conflict with a system
        //   mdns daemon
        me.prepend("jdns-");

        if(counter > 1)
            me += QString("-%1").arg(counter);

        host = escapeDomainPart(me.toUtf8()) + ".local.";

        if(use6)
            tryPublish6();
        if(use4)
            tryPublish4();
    }

    void tryPublish6()
    {
        pub6.start(JDnsPublishAddress::IPv6, host);
    }

    void tryPublish4()
    {
        pub4.start(JDnsPublishAddress::IPv4, host);
    }

    void tryDone()
    {
        bool done = true;
        if(use6 && !have6)
            done = false;
        if(use4 && !have4)
            done = false;

        if(done)
        {
            success = true;
            emit hostName(host);
        }
    }

    void handleFail()
    {
        // we get here if we fail to publish at all, or if we
        //   successfully publish but then fail later on.  in the
        //   latter case it means we "lost" our host records.

        bool lostHost = success; // as in earlier publish success
        success = false;

        // if we lost a hostname with a suffix, or counter is
        //   at 99, then start counter over at 1 (no suffix).
        if((lostHost && counter > 1) || counter >= 99)
            counter = 1;
        else
            ++counter;

        tryPublish();

        // only emit lost host signal once
        if(lostHost)
            emit hostName(QByteArray());
    }

private slots:
    void doDisable()
    {
        bool lostHost = success;
        success = false;

        if(lostHost)
            emit hostName(QByteArray());
    }

    void pub6_ready()
    {
        if(pub6.success())
        {
            have6 = true;
            tryDone();
        }
        else
        {
            have6 = false;
            have4 = false;
            pub4.cancel();
            handleFail();
        }
    }

    void pub4_ready()
    {
        if(pub4.success())
        {
            have4 = true;
            tryDone();
        }
        else
        {
            have4 = false;
            have6 = false;
            pub6.cancel();
            handleFail();
        }
    }
};

//----------------------------------------------------------------------------
// JDnsPublish
//----------------------------------------------------------------------------
class JDnsPublish;

class JDnsPublishExtra : public QObject
{
    Q_OBJECT

public:
    JDnsPublishExtra(JDnsPublish *_jdnsPub);
    ~JDnsPublishExtra();

    void start(const QJDns::Record &_rec);
    void update(const QJDns::Record &_rec);

signals:
    void published();
    void error(QJDnsSharedRequest::Error e);

private:
    friend class JDnsPublish;

    JDnsPublish *jdnsPub;
    bool started;
    QJDnsSharedRequest pub;
    QJDns::Record rec;
    bool have;
    bool need_update;
};

// This class publishes SRV/TXT/PTR for a service.  if a hostName is not
//   is not available (see JDnsPublishAddresses) then the publish action
//   will be deferred until one is available.  SRV and TXT are published
//   as unique records, and once they both succeed then the PTR record
//   is published.  once the PTR succeeds, then published() is emitted.
//   if a conflict occurs with any action, then the whole thing fails and
//   error() is emitted.  if, at any time, the hostName is lost, then
//   then the SRV operation is canceled, but no error is emitted.  when the
//   hostName is regained, then the SRV record is republished.
//
// It's important to note that published() is only emitted once ever, even
//   if a hostName change causes a republishing.  this way, hostName changes
//   are completely transparent.
class JDnsPublish : public QObject
{
    Q_OBJECT

public:
    QJDnsShared *jdns;
    QJDnsSharedRequest pub_srv;
    QJDnsSharedRequest pub_txt;
    QJDnsSharedRequest pub_ptr;

    bool have_srv, have_txt, have_ptr;
    bool need_update_txt;

    QByteArray fullname;
    QByteArray instance;
    QByteArray type;
    QByteArray host;
    int port;
    QList<QByteArray> attribs;

    QSet<JDnsPublishExtra*> extraList;

    JDnsPublish(QJDnsShared *_jdns, QObject *parent = 0) :
        QObject(parent),
        jdns(_jdns),
        pub_srv(_jdns, this),
        pub_txt(_jdns, this),
        pub_ptr(_jdns, this)
    {
        connect(&pub_srv, SIGNAL(resultsReady()), SLOT(pub_srv_ready()));
        connect(&pub_txt, SIGNAL(resultsReady()), SLOT(pub_txt_ready()));
        connect(&pub_ptr, SIGNAL(resultsReady()), SLOT(pub_ptr_ready()));
    }

    ~JDnsPublish()
    {
        qDeleteAll(extraList);
    }

    void start(const QString &_instance, const QByteArray &_type, const QByteArray &localHost, int _port, const QMap<QString,QByteArray> &attributes)
    {
        type = _type;
        Q_ASSERT(validServiceType(type));

        instance = escapeDomainPart(_instance.toUtf8());
        fullname = instance + '.' + type + ".local.";
        host = localHost;
        port = _port;
        attribs = makeTxtList(attributes);

        have_srv = false;
        have_txt = false;
        have_ptr = false;
        need_update_txt = false;

        // no host?  defer publishing till we have one
        if(host.isEmpty())
            return;

        doPublish();
    }

    void update(const QMap<QString,QByteArray> &attributes)
    {
        attribs = makeTxtList(attributes);

        // still publishing the initial txt?
        if(!have_txt)
        {
            // flag that we want to update once the publish
            //   succeeds.
            need_update_txt = true;
            return;
        }

        // no SRV, but have TXT?  this means we lost SRV due to
        //   a hostname change.
        if(!have_srv)
        {
            // in that case, revoke the TXT.  it'll get
            //   republished after SRV then.
            have_txt = false;
            pub_txt.cancel();
            return;
        }

        doPublishTxt();
    }

public slots:
    // pass empty host if host lost
    void hostChanged(const QByteArray &_host)
    {
        bool changed = (host != _host);

        if(changed)
        {
            host = _host;

            if(host.isEmpty())
            {
                // cancel srv record momentarily
                have_srv = false;
                pub_srv.cancel();
            }
            else
            {
                // we now have a host, publish
                doPublish();
            }
        }
    }

signals:
    void published();
    void error(QJDnsSharedRequest::Error e);

private:
    friend class JDnsPublishExtra;

    static QList<QByteArray> makeTxtList(const QMap<QString,QByteArray> &attributes)
    {
        QList<QByteArray> out;

        QMapIterator<QString,QByteArray> it(attributes);
        while(it.hasNext())
        {
            it.next();
            out += it.key().toLatin1() + '=' + it.value();
        }
        if(out.isEmpty())
            out += QByteArray();

        return out;
    }

    void doPublish()
    {
        // SRV
        QJDns::Record rec;
        rec.type = QJDns::Srv;
        rec.owner = fullname;
        rec.ttl = 120;
        rec.haveKnown = true;
        rec.name = host;
        rec.port = port;
        rec.priority = 0;
        rec.weight = 0;
        pub_srv.publish(QJDns::Unique, rec);

        // if we're just republishing SRV after losing/regaining
        //   our hostname, then TXT is already published
        if(!have_txt)
            doPublishTxt();

        // publish extra records as needed
        foreach(JDnsPublishExtra *extra, extraList)
        {
            if(!extra->have)
                doPublishExtra(extra);
        }
    }

    void doPublishTxt()
    {
        // TXT
        QJDns::Record rec;
        rec.type = QJDns::Txt;
        rec.owner = fullname;
        rec.ttl = 4500;
        rec.haveKnown = true;
        rec.texts = attribs;

        if(!have_txt)
            pub_txt.publish(QJDns::Unique, rec);
        else
            pub_txt.publishUpdate(rec);
    }

    void tryDone()
    {
        if(have_srv && have_txt)
        {
            // PTR
            QJDns::Record rec;
            rec.type = QJDns::Ptr;
            rec.owner = type + ".local.";
            rec.ttl = 4500;
            rec.haveKnown = true;
            rec.name = fullname;
            pub_ptr.publish(QJDns::Shared, rec);
        }
    }

    void cleanup()
    {
        foreach(JDnsPublishExtra *extra, extraList)
            cleanupExtra(extra);
        qDeleteAll(extraList);
        extraList.clear();

        have_srv = false;
        have_txt = false;
        have_ptr = false;
        pub_srv.cancel();
        pub_txt.cancel();
        pub_ptr.cancel();
    }

    void publishExtra(JDnsPublishExtra *extra)
    {
        Q_ASSERT(!extraList.contains(extra));

        connect(&extra->pub, SIGNAL(resultsReady()), SLOT(pub_extra_ready()));
        extraList += extra;

        // defer publishing until SRV is ready
        if(!have_srv)
            return;

        doPublishExtra(extra);
    }

    void publishExtraUpdate(JDnsPublishExtra *extra)
    {
        if(!extra->have)
        {
            extra->need_update = true;
            return;
        }

        if(!have_srv)
        {
            extra->have = false;
            extra->pub.cancel();
            return;
        }

        doPublishExtra(extra);
    }

    void unpublishExtra(JDnsPublishExtra *extra)
    {
        extraList.remove(extra);
    }

    void doPublishExtra(JDnsPublishExtra *extra)
    {
        if(!extra->have)
            extra->pub.publish(QJDns::Unique, extra->rec);
        else
            extra->pub.publishUpdate(extra->rec);
    }

    void cleanupExtra(JDnsPublishExtra *extra)
    {
        extra->pub.cancel();
        extra->disconnect(this);
        extra->started = false;
        extra->have = false;
    }

private slots:
    void pub_srv_ready()
    {
        if(pub_srv.success())
        {
            have_srv = true;
            tryDone();
        }
        else
        {
            QJDnsSharedRequest::Error e = pub_srv.error();
            cleanup();
            emit error(e);
        }
    }

    void pub_txt_ready()
    {
        if(pub_txt.success())
        {
            have_txt = true;

            if(need_update_txt)
            {
                need_update_txt = false;
                doPublishTxt();
            }

            tryDone();
        }
        else
        {
            QJDnsSharedRequest::Error e = pub_txt.error();
            cleanup();
            emit error(e);
        }
    }

    void pub_ptr_ready()
    {
        if(pub_ptr.success())
        {
            have_ptr = true;
            emit published();
        }
        else
        {
            QJDnsSharedRequest::Error e = pub_ptr.error();
            cleanup();
            emit error(e);
        }
    }

    void pub_extra_ready()
    {
        QJDnsSharedRequest *req = static_cast<QJDnsSharedRequest *>(sender());
        JDnsPublishExtra *extra = 0;
        foreach(JDnsPublishExtra *e, extraList)
        {
            if(&e->pub == req)
            {
                extra = e;
                break;
            }
        }
        Q_ASSERT(extra);

        if(extra->pub.success())
        {
            extra->have = true;

            if(extra->need_update)
            {
                extra->need_update = false;
                doPublishExtra(extra);
            }

            emit extra->published();
        }
        else
        {
            QJDnsSharedRequest::Error e = extra->pub.error();
            cleanupExtra(extra);
            emit extra->error(e);
        }
    }
};

JDnsPublishExtra::JDnsPublishExtra(JDnsPublish *_jdnsPub) :
    QObject(_jdnsPub),
    jdnsPub(_jdnsPub),
    started(false),
    pub(_jdnsPub->jdns, this)
{
}

JDnsPublishExtra::~JDnsPublishExtra()
{
    if(started)
        jdnsPub->unpublishExtra(this);
}

void JDnsPublishExtra::start(const QJDns::Record &_rec)
{
    rec = _rec;
    started = true;
    have = false;
    need_update = false;
    jdnsPub->publishExtra(this);
}

void JDnsPublishExtra::update(const QJDns::Record &_rec)
{
    rec = _rec;
    jdnsPub->publishExtraUpdate(this);
}

//----------------------------------------------------------------------------
// JDnsServiceProvider
//----------------------------------------------------------------------------
class BrowseItem
{
public:
    const int id;
    JDnsBrowse * const browse;
    ObjectSession *sess;

    BrowseItem(int _id, JDnsBrowse *_browse) :
        id(_id),
        browse(_browse),
        sess(0)
    {
    }

    ~BrowseItem()
    {
        delete browse;
        delete sess;
    }
};

class BrowseItemList
{
private:
    QSet<BrowseItem*> items;
    QHash<int,BrowseItem*> indexById;
    QHash<JDnsBrowse*,BrowseItem*> indexByBrowse;
    IdManager idman;

public:
    ~BrowseItemList()
    {
        qDeleteAll(items);
    }

    int reserveId()
    {
        return idman.reserveId();
    }

    void insert(BrowseItem *item)
    {
        items.insert(item);
        indexById.insert(item->id, item);
        indexByBrowse.insert(item->browse, item);
    }

    void remove(BrowseItem *item)
    {
        indexById.remove(item->id);
        indexByBrowse.remove(item->browse);
        items.remove(item);
        if(item->id != -1)
            idman.releaseId(item->id);
        delete item;
    }

    BrowseItem *itemById(int id) const
    {
        return indexById.value(id);
    }

    BrowseItem *itemByBrowse(JDnsBrowse *browse) const
    {
        return indexByBrowse.value(browse);
    }
};

class ResolveItem
{
public:
    const int id;
    JDnsServiceResolve * const resolve;
    ObjectSession *sess;

    ResolveItem(int _id, JDnsServiceResolve *_resolve) :
        id(_id),
        resolve(_resolve),
        sess(0)
    {
    }

    ~ResolveItem()
    {
        delete resolve;
        delete sess;
    }
};

class ResolveItemList
{
private:
    QSet<ResolveItem*> items;
    QHash<int,ResolveItem*> indexById;
    QHash<JDnsServiceResolve*,ResolveItem*> indexByResolve;
    IdManager idman;

public:
    ~ResolveItemList()
    {
        qDeleteAll(items);
    }

    int reserveId()
    {
        return idman.reserveId();
    }

    void insert(ResolveItem *item)
    {
        items.insert(item);
        indexById.insert(item->id, item);
        indexByResolve.insert(item->resolve, item);
    }

    void remove(ResolveItem *item)
    {
        indexById.remove(item->id);
        indexByResolve.remove(item->resolve);
        items.remove(item);
        if(item->id != -1)
            idman.releaseId(item->id);
        delete item;
    }

    ResolveItem *itemById(int id) const
    {
        return indexById.value(id);
    }

    ResolveItem *itemByResolve(JDnsServiceResolve *resolve) const
    {
        return indexByResolve.value(resolve);
    }
};

class PublishItem
{
public:
    const int id;
    JDnsPublish * const publish;
    ObjectSession *sess;

    PublishItem(int _id, JDnsPublish *_publish) :
        id(_id),
        publish(_publish),
        sess(0)
    {
    }

    ~PublishItem()
    {
        delete publish;
        delete sess;
    }
};

class PublishItemList
{
public:
    QSet<PublishItem*> items;

private:
    QHash<int,PublishItem*> indexById;
    QHash<JDnsPublish*,PublishItem*> indexByPublish;
    IdManager idman;

public:
    ~PublishItemList()
    {
        qDeleteAll(items);
    }

    int reserveId()
    {
        return idman.reserveId();
    }

    void insert(PublishItem *item)
    {
        items.insert(item);
        indexById.insert(item->id, item);
        indexByPublish.insert(item->publish, item);
    }

    void remove(PublishItem *item)
    {
        indexById.remove(item->id);
        indexByPublish.remove(item->publish);
        items.remove(item);
        if(item->id != -1)
            idman.releaseId(item->id);
        delete item;
    }

    PublishItem *itemById(int id) const
    {
        return indexById.value(id);
    }

    PublishItem *itemByPublish(JDnsPublish *publish) const
    {
        return indexByPublish.value(publish);
    }
};

class PublishExtraItem
{
public:
    const int id;
    JDnsPublishExtra * const publish;
    ObjectSession *sess;

    PublishExtraItem(int _id, JDnsPublishExtra *_publish) :
        id(_id),
        publish(_publish),
        sess(0)
    {
    }

    ~PublishExtraItem()
    {
        delete publish;
        delete sess;
    }
};

class PublishExtraItemList
{
public:
    QSet<PublishExtraItem*> items;

private:
    QHash<int,PublishExtraItem*> indexById;
    QHash<JDnsPublishExtra*,PublishExtraItem*> indexByPublish;
    IdManager idman;

public:
    ~PublishExtraItemList()
    {
        qDeleteAll(items);
    }

    void clear()
    {
        qDeleteAll(items);
        items.clear();
        indexById.clear();
        indexByPublish.clear();
        idman.clear();
    }

    int reserveId()
    {
        return idman.reserveId();
    }

    void insert(PublishExtraItem *item)
    {
        items.insert(item);
        indexById.insert(item->id, item);
        indexByPublish.insert(item->publish, item);
    }

    void remove(PublishExtraItem *item)
    {
        indexById.remove(item->id);
        indexByPublish.remove(item->publish);
        items.remove(item);
        if(item->id != -1)
            idman.releaseId(item->id);
        delete item;
    }

    PublishExtraItem *itemById(int id) const
    {
        return indexById.value(id);
    }

    PublishExtraItem *itemByPublish(JDnsPublishExtra *publish) const
    {
        return indexByPublish.value(publish);
    }
};

class JDnsServiceProvider : public ServiceProvider
{
    Q_OBJECT

public:
    JDnsGlobal *global;

    // browse
    BrowseItemList browseItemList;
    QHash<QByteArray,ServiceInstance> items;

    // resolve
    ResolveItemList resolveItemList;

    // publish
    JDnsPublishAddresses *pub_addresses;
    QByteArray localHost;
    PublishItemList publishItemList;
    PublishExtraItemList publishExtraItemList;

    static JDnsServiceProvider *create(JDnsGlobal *global, QObject *parent = 0)
    {
        return new JDnsServiceProvider(global, parent);
    }

    JDnsServiceProvider(JDnsGlobal *_global, QObject *parent = 0) :
        ServiceProvider(parent),
        pub_addresses(0)
    {
        global = _global;
        connect(global, SIGNAL(interfacesChanged()), SLOT(interfacesChanged()));
    }

    ~JDnsServiceProvider()
    {
        // make sure extra items are deleted before normal ones
        publishExtraItemList.clear();
    }

    virtual int browse_start(const QString &_type, const QString &_domain)
    {
        QString domain;
        if(_domain.isEmpty() || _domain == ".")
            domain = "local.";
        else
            domain = _domain;

        if(domain[domain.length() - 1] != '.')
            domain += '.';

        Q_ASSERT(domain.length() >= 2 && domain[domain.length() - 1] == '.');

        int id = browseItemList.reserveId();

        // no support for non-local domains
        if(domain != "local.")
        {
            BrowseItem *i = new BrowseItem(id, 0);
            i->sess = new ObjectSession(this);
            browseItemList.insert(i);
            i->sess->defer(this, "do_browse_error", Q_ARG(int, i->id),
                Q_ARG(XMPP::ServiceBrowser::Error, ServiceBrowser::ErrorNoWide));
            return i->id;
        }

        if(!global->ensure_mul())
        {
            BrowseItem *i = new BrowseItem(id, 0);
            i->sess = new ObjectSession(this);
            browseItemList.insert(i);
            i->sess->defer(this, "do_browse_error", Q_ARG(int, i->id),
                Q_ARG(XMPP::ServiceBrowser::Error, ServiceBrowser::ErrorNoLocal));
            return i->id;
        }

        QByteArray type = _type.toUtf8();
        if(!validServiceType(type))
        {
            BrowseItem *i = new BrowseItem(id, 0);
            i->sess = new ObjectSession(this);
            browseItemList.insert(i);
            i->sess->defer(this, "do_browse_error", Q_ARG(int, i->id),
                Q_ARG(XMPP::ServiceBrowser::Error, ServiceBrowser::ErrorGeneric));
            return i->id;
        }

        BrowseItem *i = new BrowseItem(id, new JDnsBrowse(global->mul, this));
        connect(i->browse, SIGNAL(available(QByteArray)), SLOT(jb_available(QByteArray)));
        connect(i->browse, SIGNAL(unavailable(QByteArray)), SLOT(jb_unavailable(QByteArray)));
        browseItemList.insert(i);
        i->browse->start(type);
        return i->id;
    }

    virtual void browse_stop(int id)
    {
        BrowseItem *i = browseItemList.itemById(id);
        Q_ASSERT(i);

        browseItemList.remove(i);
    }

    virtual int resolve_start(const QByteArray &name)
    {
        int id = resolveItemList.reserveId();

        if(!global->ensure_mul())
        {
            ResolveItem *i = new ResolveItem(id, 0);
            i->sess = new ObjectSession(this);
            resolveItemList.insert(i);
            i->sess->defer(this, "do_resolve_error", Q_ARG(int, i->id),
                Q_ARG(XMPP::ServiceResolver::Error, ServiceResolver::ErrorNoLocal));
            return i->id;
        }

        ResolveItem *i = new ResolveItem(id, new JDnsServiceResolve(global->mul, this));
        connect(i->resolve, SIGNAL(finished()), SLOT(jr_finished()));
        connect(i->resolve, SIGNAL(error(QJDnsSharedRequest::Error)), SLOT(jr_error(QJDnsSharedRequest::Error)));
        resolveItemList.insert(i);
        i->resolve->start(name);
        return i->id;
    }

    virtual void resolve_stop(int id)
    {
        ResolveItem *i = resolveItemList.itemById(id);
        Q_ASSERT(i);

        resolveItemList.remove(i);
    }

    virtual int publish_start(const QString &instance, const QString &_type, int port, const QMap<QString,QByteArray> &attributes)
    {
        int id = publishItemList.reserveId();

        if(!global->ensure_mul())
        {
            PublishItem *i = new PublishItem(id, 0);
            i->sess = new ObjectSession(this);
            publishItemList.insert(i);
            i->sess->defer(this, "do_publish_error", Q_ARG(int, i->id),
                Q_ARG(XMPP::ServiceLocalPublisher::Error, ServiceLocalPublisher::ErrorNoLocal));
            return i->id;
        }

        QByteArray type = _type.toUtf8();
        if(!validServiceType(type))
        {
            PublishItem *i = new PublishItem(id, 0);
            i->sess = new ObjectSession(this);
            publishItemList.insert(i);
            i->sess->defer(this, "do_publish_error", Q_ARG(int, i->id),
                Q_ARG(XMPP::ServiceLocalPublisher::Error, ServiceLocalPublisher::ErrorGeneric));
            return i->id;
        }

        // make sure A/AAAA records are published
        if(!pub_addresses)
        {
            pub_addresses = new JDnsPublishAddresses(global->mul, this);
            connect(pub_addresses, SIGNAL(hostName(QByteArray)), SLOT(pub_addresses_hostName(QByteArray)));
            pub_addresses->setUseIPv6(global->haveMulticast6());
            pub_addresses->setUseIPv4(global->haveMulticast4());
            pub_addresses->start();
        }

        // it's okay to attempt to publish even if pub_addresses
        //   hasn't succeeded yet.  JDnsPublish is smart enough to
        //   defer the operation until a host is acquired.
        PublishItem *i = new PublishItem(id, new JDnsPublish(global->mul, this));
        connect(i->publish, SIGNAL(published()), SLOT(jp_published()));
        connect(i->publish, SIGNAL(error(QJDnsSharedRequest::Error)), SLOT(jp_error(QJDnsSharedRequest::Error)));
        publishItemList.insert(i);
        i->publish->start(instance, type, localHost, port, attributes);
        return i->id;
    }

    virtual void publish_update(int id, const QMap<QString,QByteArray> &attributes)
    {
        PublishItem *i = publishItemList.itemById(id);
        Q_ASSERT(i);

        // if we already have an error queued, do nothing
        if(i->sess->isDeferred(this, "do_publish_error"))
            return;

        i->publish->update(attributes);
    }

    virtual void publish_stop(int id)
    {
        PublishItem *i = publishItemList.itemById(id);
        Q_ASSERT(i);

        cleanupExtra(i);
        publishItemList.remove(i);
    }

    virtual int publish_extra_start(int pub_id, const NameRecord &name)
    {
        PublishItem *pi = publishItemList.itemById(pub_id);
        Q_ASSERT(pi);

        int id = publishItemList.reserveId();

        QJDns::Record rec = exportJDNSRecord(name);
        if(rec.type == -1)
        {
            PublishExtraItem *i = new PublishExtraItem(id, 0);
            i->sess = new ObjectSession(this);
            publishExtraItemList.insert(i);
            i->sess->defer(this, "do_publish_extra_error", Q_ARG(int, i->id),
                Q_ARG(XMPP::ServiceLocalPublisher::Error, ServiceLocalPublisher::ErrorGeneric));
            return i->id;
        }

        // fill in owner if necessary
        if(rec.owner.isEmpty())
            rec.owner = pi->publish->fullname;

        // fill in the ttl if necessary
        if(rec.ttl == 0)
            rec.ttl = 4500;

        PublishExtraItem *i = new PublishExtraItem(id, new JDnsPublishExtra(pi->publish));
        connect(i->publish, SIGNAL(published()), SLOT(jpe_published()));
        connect(i->publish, SIGNAL(error(QJDnsSharedRequest::Error)), SLOT(jpe_error(QJDnsSharedRequest::Error)));
        publishExtraItemList.insert(i);
        i->publish->start(rec);
        return i->id;
    }

    virtual void publish_extra_update(int id, const NameRecord &name)
    {
        PublishExtraItem *i = publishExtraItemList.itemById(id);
        Q_ASSERT(i);

        // if we already have an error queued, do nothing
        if(i->sess->isDeferred(this, "do_publish_extra_error"))
            return;

        QJDns::Record rec = exportJDNSRecord(name);
        if(rec.type == -1)
        {
            i->sess = new ObjectSession(this);
            i->sess->defer(this, "do_publish_extra_error", Q_ARG(int, i->id),
                Q_ARG(XMPP::ServiceLocalPublisher::Error, ServiceLocalPublisher::ErrorGeneric));
            return;
        }

        // fill in owner if necessary
        if(rec.owner.isEmpty())
            rec.owner = static_cast<JDnsPublish*>(i->publish->parent())->fullname;

        // fill in the ttl if necessary
        if(rec.ttl == 0)
            rec.ttl = 4500;

        i->publish->update(rec);
    }

    virtual void publish_extra_stop(int id)
    {
        PublishExtraItem *i = publishExtraItemList.itemById(id);
        Q_ASSERT(i);

        publishExtraItemList.remove(i);
    }

private:
    void cleanupExtra(PublishItem *pi)
    {
        // remove all extra publishes associated with this publish.
        //   the association can be checked via QObject parenting.
        QSet<PublishExtraItem*> remove;
        foreach(PublishExtraItem *i, publishExtraItemList.items)
        {
            if(static_cast<JDnsPublish*>(i->publish->parent()) == pi->publish)
                remove += i;
        }

        foreach(PublishExtraItem *i, remove)
            publishExtraItemList.remove(i);
    }

private slots:
    void interfacesChanged()
    {
        if(pub_addresses)
        {
            pub_addresses->setUseIPv6(global->haveMulticast6());
            pub_addresses->setUseIPv4(global->haveMulticast4());
        }
    }

    void jb_available(const QByteArray &instance)
    {
        JDnsBrowse *jb = static_cast<JDnsBrowse *>(sender());
        BrowseItem *i = browseItemList.itemByBrowse(jb);
        Q_ASSERT(i);

        QByteArray name = instance + '.' + jb->typeAndDomain;
        ServiceInstance si(QString::fromLatin1(instance), QString::fromLatin1(jb->type), "local.", QMap<QString,QByteArray>());
        items.insert(name, si);

        emit browse_instanceAvailable(i->id, si);
    }

    void jb_unavailable(const QByteArray &instance)
    {
        JDnsBrowse *jb = static_cast<JDnsBrowse *>(sender());
        BrowseItem *i = browseItemList.itemByBrowse(jb);
        Q_ASSERT(i);

        QByteArray name = instance + '.' + jb->typeAndDomain;
        Q_ASSERT(items.contains(name));

        ServiceInstance si = items.value(name);
        items.remove(name);

        emit browse_instanceUnavailable(i->id, si);
    }

    void do_browse_error(int id, XMPP::ServiceBrowser::Error e)
    {
        BrowseItem *i = browseItemList.itemById(id);
        Q_ASSERT(i);

        browseItemList.remove(i);
        emit browse_error(id, e);
    }

    void jr_finished()
    {
        JDnsServiceResolve *jr = static_cast<JDnsServiceResolve *>(sender());
        ResolveItem *i = resolveItemList.itemByResolve(jr);
        Q_ASSERT(i);

        // parse TXT list into attribute map
        QMap<QString,QByteArray> attribs;
        for(int n = 0; n < jr->attribs.count(); ++n)
        {
            const QByteArray &a = jr->attribs[n];
            QString key;
            QByteArray value;
            int x = a.indexOf('=');
            if(x != -1)
            {
                key = QString::fromLatin1(a.mid(0, x));
                value = a.mid(x + 1);
            }
            else
            {
                key = QString::fromLatin1(a);
            }

            attribs.insert(key, value);
        }

        // one of these must be true
        Q_ASSERT(jr->have4 || jr->have6);

        QList<ResolveResult> results;
        if(jr->have6)
        {
            ResolveResult r;
            r.attributes = attribs;
            r.address = jr->addr6;
            r.port = jr->port;
            r.hostName = jr->host;
            results += r;
        }
        if(jr->have4)
        {
            ResolveResult r;
            r.attributes = attribs;
            r.address = jr->addr4;
            r.port = jr->port;
            r.hostName = jr->host;
            results += r;
        }

        int id = i->id;
        resolveItemList.remove(i);
        emit resolve_resultsReady(id, results);
    }

    void jr_error(QJDnsSharedRequest::Error e)
    {
        JDnsServiceResolve *jr = static_cast<JDnsServiceResolve *>(sender());
        ResolveItem *i = resolveItemList.itemByResolve(jr);
        Q_ASSERT(i);

        ServiceResolver::Error err;
        if(e == QJDnsSharedRequest::ErrorTimeout)
            err = ServiceResolver::ErrorTimeout;
        else
            err = ServiceResolver::ErrorGeneric;

        int id = i->id;
        resolveItemList.remove(i);
        emit resolve_error(id, err);
    }

    void do_resolve_error(int id, XMPP::ServiceResolver::Error e)
    {
        ResolveItem *i = resolveItemList.itemById(id);
        Q_ASSERT(i);

        resolveItemList.remove(i);
        emit resolve_error(id, e);
    }

    void pub_addresses_hostName(const QByteArray &name)
    {
        // tell all active publishes about the change
        foreach(PublishItem *item, publishItemList.items)
            item->publish->hostChanged(name);
    }

    void jp_published()
    {
        JDnsPublish *jp = static_cast<JDnsPublish *>(sender());
        PublishItem *i = publishItemList.itemByPublish(jp);
        Q_ASSERT(i);

        emit publish_published(i->id);
    }

    void jp_error(QJDnsSharedRequest::Error e)
    {
        JDnsPublish *jp = static_cast<JDnsPublish *>(sender());
        PublishItem *i = publishItemList.itemByPublish(jp);
        Q_ASSERT(i);

        ServiceLocalPublisher::Error err;
        if(e == QJDnsSharedRequest::ErrorConflict)
            err = ServiceLocalPublisher::ErrorConflict;
        else
            err = ServiceLocalPublisher::ErrorGeneric;

        int id = i->id;
        cleanupExtra(i);
        publishItemList.remove(i);
        emit publish_error(id, err);
    }

    void do_publish_error(int id, XMPP::ServiceLocalPublisher::Error e)
    {
        PublishItem *i = publishItemList.itemById(id);
        Q_ASSERT(i);

        cleanupExtra(i);
        publishItemList.remove(i);
        emit publish_error(id, e);
    }

    void jpe_published()
    {
        JDnsPublishExtra *jp = static_cast<JDnsPublishExtra *>(sender());
        PublishExtraItem *i = publishExtraItemList.itemByPublish(jp);
        Q_ASSERT(i);

        emit publish_extra_published(i->id);
    }

    void jpe_error(QJDnsSharedRequest::Error e)
    {
        JDnsPublishExtra *jp = static_cast<JDnsPublishExtra *>(sender());
        PublishExtraItem *i = publishExtraItemList.itemByPublish(jp);
        Q_ASSERT(i);

        ServiceLocalPublisher::Error err;
        if(e == QJDnsSharedRequest::ErrorConflict)
            err = ServiceLocalPublisher::ErrorConflict;
        else
            err = ServiceLocalPublisher::ErrorGeneric;

        int id = i->id;
        publishExtraItemList.remove(i);
        emit publish_extra_error(id, err);
    }

    void do_publish_extra_error(int id, XMPP::ServiceLocalPublisher::Error e)
    {
        PublishExtraItem *i = publishExtraItemList.itemById(id);
        Q_ASSERT(i);

        publishExtraItemList.remove(i);
        emit publish_extra_error(id, e);
    }
};

//----------------------------------------------------------------------------
// JDnsProvider
//----------------------------------------------------------------------------
class JDnsProvider : public IrisNetProvider
{
    Q_OBJECT
    Q_INTERFACES(XMPP::IrisNetProvider)

public:
    JDnsGlobal *global;

    JDnsProvider()
    {
        global = 0;
    }

    ~JDnsProvider()
    {
        delete global;
    }

    void ensure_global()
    {
        if(!global)
            global = new JDnsGlobal;
    }

    virtual NameProvider *createNameProviderInternet()
    {
        ensure_global();
        return JDnsNameProvider::create(global, JDnsNameProvider::Internet);
    }

    virtual NameProvider *createNameProviderLocal()
    {
        ensure_global();
        return JDnsNameProvider::create(global, JDnsNameProvider::Local);
    }

    virtual ServiceProvider *createServiceProvider()
    {
        ensure_global();
        return JDnsServiceProvider::create(global);
    }
};

IrisNetProvider *irisnet_createJDnsProvider()
{
        return new JDnsProvider;
}

}

#include "netnames_jdns.moc"
