//-*-c++-*-
/***************************************************************************
 *   Copyright (C) 2003 by Fred Schaettgen                                 *
 *   kdebluetooth@schaettgen.de                                            *
 *                                                                         *
 *   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.                                   *
 ***************************************************************************/

#include "btsdp.h"

#include <sys/stat.h>
#include <qregexp.h>
#include <algorithm>
#include <kdebug.h>
#include <kinstance.h>
#include <klocale.h>
#include <kservice.h>
#include <kservicetype.h>
#include <dcopclient.h>

#include <libkbluetooth/sdpdevice.h>
#include <libkbluetooth/inquiry.h>
#include <libkbluetooth/devicemimeconverter.h>
#include <libkbluetooth/adapter.h>
#include <libkbluetooth/namecache.h>

using namespace KIO;
using namespace KBluetooth;
using namespace std;

SdpProtocol::SdpProtocol(const QCString &pool_socket, const QCString &app_socket) :
        SlaveBase("kio_sdp", pool_socket, app_socket)
{
    kdDebug() << "SdpProtocol::SdpProtocol()" << endl;
    serviceListUp2Date = false;
    DevInfo localInfo;
    localInfo.realName = localInfo.uniqueName = "localhost";
    localInfo.address = DeviceAddress("FF:FF:FF:00:00:00");
    deviceList.push_back(localInfo);
    initHandlerList();
    if (Adapters().count() == 0) {
        warning(i18n("No working Bluetooth adapter found."));
    }
}

SdpProtocol::~SdpProtocol()
{
    kdDebug() << "SdpProtocol::~SdpProtocol()" << endl;
}


void SdpProtocol::initHandlerList()
{
    KService::List handlers =
        KServiceType::offers("SdpServiceHandler");
    kdDebug() << "Looking for ServiceHandlers" << endl;
    for(KService::List::ConstIterator it = handlers.begin();
            it != handlers.end(); it++)
    {
        KService::Ptr s = *it;
        QStringList sAttributes;
        QVariant vProtocol = s->property(
                                 "X-SDPSERVICEHANDLER-requiredUIDs");
        QVariant vMimetype = s->property("X-SDPSERVICEHANDLER-mimeType");
        kdDebug() << "Allowed attributes:"
        << s->propertyNames().join(",") << endl;
        if (vProtocol.isValid() && vMimetype.isValid())
        {
            kdDebug() << "ServiceUIDs:" <<
            vProtocol.toStringList().join(" & ") <<
            " mimetype:" << vMimetype.toString() << endl;

            ServiceHandlerInfo handlerInfo;
            handlerInfo.mimeType = vMimetype.toString();

            // Parse the UUID list of the service Handler
            QStringList uuidStrList = vProtocol.toStringList();
            for (uint n=0; n<uuidStrList.size(); ++n)
            {
                SDP::uuid_t uuid;
                kdDebug() << "sdp: adding uuid " << uuidStrList[n] << endl;
                if (uuid.fromString(uuidStrList[n]) == true)
                {
                    kdDebug() << "->" << QString(uuid) << endl;
                    ;
                    handlerInfo.uuids.push_back(uuid);
                }
                else
                {
                    kdWarning() << QString("Bad uuid (%1) for service %2").
                    arg(uuidStrList[n]).arg(s->name()) << endl;
                }
            }

            // Append the handler info to the handlerList
            this->handlerList.push_back(handlerInfo);
        }
        else
        {
            kdDebug() << "Service has wrong UID List" << endl;
        }
    }
    kdDebug() << "done." << endl;
}


void SdpProtocol::setHost(const QString & host, int /*port*/,
                              const QString &/*user*/, const QString &/*pass*/)
{
    kdDebug() << "kio_sdp::setHost(" << host << ")" << endl ;
    serviceListUp2Date = false;
}

/*void SdpProtocol::mimetype(const KURL &url)
{
    kdDebug(7101) << "kio_sdp::mimetype(" << url.url() << ")" << endl ;
    //mimeType("text/plain");
    finished();
}*/

void SdpProtocol::stat(const KURL &url)
{
    kdDebug() << "kio_sdp::stat(" << url.url() << ")" << endl ;
    UDSEntry entry;
    QString path = url.path(+1);
    if (url.hasHost() == true)
    {
        if (path == "/")
        {
            createDirEntry(entry, "Bluetooth neighborhood");
            statEntry(entry);
            finished();
        }
        else
        {
            QRegExp reg = QRegExp("^/uuid-(0x[a-f,A-F,0-9:]+)/");
            if (reg.search(path) >= 0) {
                createDirEntry(entry, "More services");
                statEntry(entry);
                finished();
            }
            else {
              error(KIO::ERR_SLAVE_DEFINED,
                    i18n("Could not stat %1. Unknown device").arg(url.url()));
            }

        }
    }
    else {
        redirection(KURL("bluetooth:/"));
        finished();
    }
}


void SdpProtocol::listDir(const KURL &url)
{
    QString host = url.host();
    QString path = url.path(+1);

    kdDebug() << "kio_sdp::listdir(" << host << ") (" << path <<  ")" << endl ;

    if (host == QString::null)
    {
        redirection(KURL("bluetooth:/"));
        finished();
    }
    else
    {
        if (path == "/")
        {
            doListServices(url, host, "0x1002");
        }
        else
        {
            QRegExp reg = QRegExp("^/uuid-(0x[a-f,A-F,0-9:]+)/");
            if (reg.search(path) >= 0) {
                doListServices(url, host, reg.cap(0));
            }
            else {
                doListInvalid(url);
            }
        }
    }
}

bool SdpProtocol::findDeviceByName(DevInfo &info, QString devStr)
{
    kdDebug() << "sdp::findDeviceByName(" << devStr << ")" << endl;

    // try to find the name in the device list
    DevInfoVec::iterator it;
    for (it = deviceList.begin(); it != deviceList.end(); ++it)
    {
        if (it->realName.lower() == devStr.lower()
            || it->uniqueName.lower() == devStr.lower())
        {
            info = *it;
            return true;
        }
    }

    // try to look the address up with the kbluetoothd device cache
    // Ask the kbluetoothd for a cached name for the given address
    DeviceAddress devAddr;
    int devClass = 0;
    if (NameCache::resolveCachedName(devStr, devAddr, dcopClient())) {
        if (devAddr != DeviceAddress::invalid) {
            if (NameCache::getCachedClass(devAddr, devClass, dcopClient())) {
                info.address = devAddr;
                info.mimeType = DeviceClassMimeConverter::classToMimeType(devClass);
                info.realName = devStr;
                info.uniqueName = devStr;
                return true;
            }
            else {
                kdWarning() << "DCOPcall getCachedDeviceClass() failed" << endl;
            }
         }
    }
    else {
        kdWarning() << "DCOPcall resolveCachedDeviceName() failed" << endl;
    }

    // Does the device name look like a bluetooth address?
    QRegExp reg = QRegExp("^(?:[a-f,A-F,0-9]{2}:){5}[a-f,A-F,0-9]{2}$");
    if (reg.search(devStr) >= 0)
    {
        return findDeviceByAddress(info, DeviceAddress(devStr));
    }
    else
    {
        // if the device isn't in the list already, we do an
        // inquiry and add all devices to the database
        Inquiry inquiry;
        infoMessage(i18n("Trying to a device called \"%1\"...").arg(devStr));
        inquiry.inquiry();
        DeviceAddress inqAddr;
        int inqClass;
        bool bFound = false;
        DevInfo foundInfo;
        while (inquiry.nextNeighbour(inqAddr, inqClass)) {
            if (findDeviceByAddress(foundInfo, inqAddr, inqClass))
            {
                if (foundInfo.realName == devStr ||
                    foundInfo.uniqueName == devStr)
                {
                    bFound = true;
                    info = foundInfo;
                }
            }
        }

        infoMessage(QString::null);
        return bFound;
    }
}

bool SdpProtocol::findDeviceByAddress(DevInfo &info,
    DeviceAddress addr, int deviceClass)
{
    kdDebug() << "sdp::findDeviceByAddress(" << QString(addr) << ")" << endl;
    // try to find the address in the device list
    DevInfoVec::iterator it;
    for (it = deviceList.begin(); it != deviceList.end(); ++it)
    {
        if (it->address == addr)
        {
            it->mimeType = DeviceClassMimeConverter::classToMimeType(deviceClass);
            info = *it;
            return true;
        }
    }

    // ..if we didn't find it we try a name request
    QString devName = nameRequester.resolve(addr);
    if (devName != QString::null)
    {
        info.address = addr;
        info.realName = devName;
        // TODO: Make the unique name really unique
        info.uniqueName = devName;
        // TODO: We get the device class during an inquiry, but
        // here we only want to do a name request. How should this
        // be handled..?
        info.mimeType = DeviceClassMimeConverter::classToMimeType(deviceClass);
        deviceList.push_back(info);
        kdDebug() << QString("Found name for %1 (%2)").
        arg(QString(addr)).arg(info.uniqueName);
        return true;
    }

    kdDebug() << "sdp::findDevice(" << QString(addr) << "): NOT FOUND! " << nameRequester.lastErrorMessage() << endl;
    return false;
}

void SdpProtocol::get(const KURL &/*url*/)
{
    kdDebug() << "kio_bluetooth: get() was called! This is nonsense." << endl ;
    error(KIO::ERR_IS_DIRECTORY, QString::null);
}

struct AddrInfo {
    DeviceAddress devAddr;
    int devClass;
    bool noname;
};

QString SdpProtocol::getCachedName(DeviceAddress addr)
{
    // Ask the kbluetoothd for a cached name for the given address
    DCOPClient* dc = dcopClient();

    QByteArray param;
    QDataStream paramStream(param, IO_WriteOnly);
    paramStream << QString(addr);
    //paramStream.close();

    QByteArray retData;

    QCString retType;
    if (dc->call( "kbluetoothd", "DeviceNameCache", "getCachedDeviceName(QString)",
        param, retType, retData, false))
    {
        QDataStream retStream(retData,IO_ReadOnly);
        QString retStr;
        retStream >> retStr;
        kdDebug() << QString("Found cached device name: %1=[%2]")
            .arg(QString(addr)).arg(retStr) << endl;
        if (retStr.length() == 0) return QString::null;
        return retStr;
    }
    else {
        kdWarning() << "DCOPcall kded::kbluetoothd::getCachedDeviceName() failed" << endl;
        return QString::null;
    }
}


bool SdpProtocol::doListServices(const KURL& url, QString hostname, const QString& browseGroup)
{
    // resolve the hostname
    DevInfo devInfo;
    if (!findDeviceByName(devInfo, hostname))
    {
        error(KIO::ERR_SLAVE_DEFINED,
              i18n("Could not find device %1.").arg(hostname));
        return false;
    }

    UDSEntry entry;
    infoMessage(i18n("Retrieving services for %1.").arg(hostname));
    SDP::Device deviceServiceInfo;
    std::set<KBluetooth::SDP::uuid_t> uuidSet;
    kdDebug() << "--[" << browseGroup << "]" << browseGroup.length() << endl;
    uuidSet.insert(KBluetooth::SDP::uuid_t(browseGroup));
    deviceServiceInfo.setTarget(devInfo.address, uuidSet);

    std::vector<SDP::Service>& services = deviceServiceInfo.services();
    kdDebug() << "Number of services: " << services.size() << endl;

    // Workaround for devices which don't have any services
    // in the public browse group
    if (services.size() == 0) {
        // search for services with the L2CAP-UUID, which should
        // catch the most common services
        uuidSet.clear();
        uuidSet.insert(KBluetooth::SDP::uuid_t("0x0100")); // L2CAP
        deviceServiceInfo.setTarget(devInfo.address, uuidSet);
        services = deviceServiceInfo.services();
    }



    infoMessage(QString::null);

    saveMru(hostname, devInfo.address);

    for (unsigned int n=0; n<services.size(); ++n)
    {
        entry.clear();
        QString serviceName;
        serviceName = "<unknown service>";
        services[n].getServiceName(serviceName);
        std::vector<SDP::uuid_t> uuids = services[n].getAllUUIDs();
        std::vector<SDP::uuid_t>:: iterator it;
        QString uuidStr = "";
        for (it = uuids.begin(); it != uuids.end(); ++it)
        {
#if (QT_VERSION >= 0x030200)
            uuidStr += QString("%1:%2 ").arg(it->hi, 1, 16).arg(it->lo, 1, 16);
#else
            uuidStr +=
              QString("%1:%2 ").arg((unsigned long int)it->hi, 1, 16)
	      .arg((unsigned long int)it->lo, 1, 16);
#endif
        }
        kdDebug() << serviceName << " UUIDs: " << uuidStr << endl ;


        KURL serviceURL;
        serviceURL.setProtocol("sdp");
        serviceURL.setHost(QString(devInfo.address));
        serviceURL.setPath("/params");
        serviceURL.addQueryItem("name", devInfo.realName);

        unsigned int rfcommChannel = 0;
        if (services[n].getRfcommChannel(rfcommChannel)==true) {
            serviceURL.addQueryItem("rfcommchannel",
                QString::number(rfcommChannel));
        }

        // Obex ftp is handled differently from the the other services
        // since in we don't want to start a program in that case,
        // but jump to an obex-url instead.
        // Should any other bluetooth-related kioslave appear eventually,
        // then we'll have to think about a solution.
        // But it seems unlikely at the moment, so we go the simple way here.
        if (std::find(uuids.begin(), uuids.end(), SDP::uuid_t("0x1106")) != uuids.end())
        {
            // OBEX ftp service:
            KURL obexFtpURL;
            obexFtpURL.setProtocol("obex");
            obexFtpURL.setHost(devInfo.address);
            if (rfcommChannel > 0) {
                obexFtpURL.setPort(rfcommChannel);
            }
            obexFtpURL.setPath("/");
            createDirEntry(entry, serviceName, obexFtpURL.url(), "bluetooth/obex-ftp-profile");
            listEntry(entry, false);
        }
        else if (std::find(uuids.begin(), uuids.end(), SDP::uuid_t("0x1105")) != uuids.end())
        {
            // OBEX Object Push service:
            // This entry is special in that it works differently depending
            // if you click the obex push icon in konqueror or in the file
            // save dialog. In konqueror, kbtobexclient is started.
            // In the file save dialog, kio_obex will be used.
            KURL obexPushURL;
            obexPushURL.setProtocol("obex");
            obexPushURL.setHost(devInfo.address);
            if (rfcommChannel > 0) {
                obexPushURL.setPort(rfcommChannel);
            }
            obexPushURL.setPath("/");
            addAtom(entry, UDS_NAME, serviceName);
            addAtom(entry, UDS_URL, obexPushURL.url());
            addAtom(entry, UDS_MIME_TYPE, "bluetooth/obex-object-push-profile");
            addAtom(entry, UDS_FILE_TYPE, S_IFDIR);
            //addAtom(entry, UDS_GUESSED_MIME_TYPE, "inode/directory");
            listEntry(entry, false);
        }
        /*else if (std::find(uuids.begin(), uuids.end(), SDP::uuid_t("0x1002")) != uuids.end())
        {
            KURL newUrl = url;
            newUrl.removeQueryItem("browsegroup");
            newUrl.addQueryItem("browsegroup", "...");
            createDirEntry(entry, serviceName, newUrl.url(), "inode/directory");
        }*/
        else {
            // the current service is not obex ftp..
            vector<QString> mimeTypes;
            findMimeTypesForUUIDList(mimeTypes, uuids);
            if (mimeTypes.size() > 0)
            {
                createFileEntry(entry, serviceName,
                                mimeTypes[0],
                                serviceURL.url());
                listEntry(entry, false);
            }
            else
            {
                createFileEntry(entry, serviceName,
                                "bluetooth/unknown-profile",
                                serviceURL.url());
                listEntry(entry, false);
            }
        }
    }


    // Add link to search for l2cap-profiles
    // if the current browse group is the public browse group
    if (browseGroup == "0x1002") {
        entry.clear();
        KURL l2capUrl = url;
        l2capUrl.setPath("/uuid-0x0100/");
        createDirEntry(entry, QString(".")+i18n("More Services"),
            l2capUrl.url(), "inode/directory");
        listEntry(entry, false);
    }


    listEntry(entry, true); // ready
    finished();

    return true;
}

// Find the mimetypes of service handlers which can
// handle a service using the given uuids
void SdpProtocol::findMimeTypesForUUIDList(
    vector<QString> &mimeTypes, const UUIDVec& uuids)
{
    mimeTypes.clear();
    HandlerInfoVec::iterator infoIt = handlerList.begin();
    // Tray each handler in handlerList
    kdDebug() << "sdp::findMimeTypesForUUIDList (" <<
    uuids.size() << " Service-UUIDs)" << endl ;
    for (; infoIt != handlerList.end(); ++infoIt)
    {
        // Check if each uuid needed by the handler
        // is defined in the uuids of the service
        kdDebug() << "sdp: trying "<< infoIt->mimeType <<
        " (" << infoIt->uuids.size() << " UUIDs)" << endl ;
        bool allUUIDsFound = true;
        UUIDVec::iterator neededIt = infoIt->uuids.begin();
        for (; neededIt != infoIt->uuids.end(); ++neededIt)
        {
            SDP::uuid_t neededUUID = *neededIt;
            bool uuidFound = false;
            UUIDVec::const_iterator uIt = uuids.begin();
            while (uIt != uuids.end() && uuidFound == false)
            {
                kdDebug() << QString("sdp: %1==%2 ?").arg(QString(neededUUID))
                .arg(QString(*uIt)) << endl;
                if (*uIt == neededUUID)
                {
                    uuidFound = true;
                }
                ++uIt;
            }
            if (uuidFound == false)
            {
                allUUIDsFound = false;
            }
        }

        if (allUUIDsFound == true)
        {
            kdDebug() << "sdp: UUID match!"<< endl ;
            mimeTypes.push_back(infoIt->mimeType);
        }
    }
}

bool SdpProtocol::doListInvalid(const KURL &url)
{
    error(KIO::ERR_MALFORMED_URL, url.url());
    return true;
}

bool SdpProtocol::createDirEntry(UDSEntry &entry, const QString &title,
    const QString &url, const QString &mimeType)
{
    entry.clear();

    addAtom(entry, UDS_NAME, title);

    if (url != QString::null)
    {
        addAtom(entry, UDS_URL, url);
    }

    addAtom(entry, UDS_MIME_TYPE, mimeType);
    addAtom(entry, UDS_FILE_TYPE, S_IFDIR);
    addAtom(entry, UDS_GUESSED_MIME_TYPE, "inode/directory");

    return true;
}

bool SdpProtocol::createFileEntry(KIO::UDSEntry &entry,
                                      QString title, QString mimetype, QString url)
{
    entry.clear();

    addAtom(entry, UDS_NAME, title);
    if (mimetype != QString::null)
    {
        addAtom(entry, UDS_MIME_TYPE, mimetype);
    }
    if (url != QString::null)
    {
        addAtom(entry, UDS_URL, url);
    }

    addAtom(entry, UDS_FILE_TYPE, S_IFREG);

    return true;
}

void SdpProtocol::addAtom(KIO::UDSEntry &entry, KIO::UDSAtomTypes type, QString s)
{
    UDSAtom atom;
    atom.m_uds = type;
    atom.m_str = s;
    entry.append(atom);
}

void SdpProtocol::addAtom(KIO::UDSEntry &entry, KIO::UDSAtomTypes type, long l)
{
    UDSAtom atom;
    atom.m_uds = type;
    atom.m_long = l;
    entry.append(atom);
}


void SdpProtocol::saveMru(QString hostname, KBluetooth::DeviceAddress addr)
{
    if (hostname == QString(addr)) {
        hostname = QString("[%1]").arg(hostname);
    }
    DCOPClient* dc = dcopClient();
    if (!dc) return;
    QByteArray param;
    QDataStream paramStream(param, IO_WriteOnly);
    QStringList command("konqueror");
    command << QString("sdp://%1/").arg(hostname.lower());
    paramStream << i18n("Service listing")
        << command
        << QString("kdebluetooth")
        << QString(addr);
    QByteArray retData;
    QCString retType;
    dc->call("kbluetoothd", "MRUServices", "mruAdd(QString,QStringList,QString,QString)",
        param, retType, retData, false);
}

extern "C"
{
    int kdemain(int argc, char **argv)
    {
        KInstance instance( "kio_sdp" );
        kdDebug() << "*** Starting kio_sdp " << endl;
        if (argc != 4)
        {
            kdDebug() << "Usage: kio_sdp  protocol domain-socket1 domain-socket2" << endl;
            exit(-1);
        }
        SdpProtocol slave(argv[2], argv[3]);
        slave.dispatchLoop();
        kdDebug(7101) << "*** kio_sdp Done" << endl;
        return 0;
    }
}
