/*
    SPDX-FileCopyrightText: 2004, 2005 Jakub Stachowski <qbast@go2.pl>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "dnssd.h"

// kioworker
#include "zeroconfurl.h"
// KF
#include <KLocalizedString>
#include <KProtocolInfo>
// Qt
#include <QCoreApplication>
#include <qplatformdefs.h> // S_IFDIR

class KIOPluginForMetaData : public QObject
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.kde.kio.worker.zeroconf" FILE "zeroconf.json")
};

void ProtocolData::feedUrl(QUrl *url, const RemoteService *remoteService) const
{
    const QMap<QString, QByteArray> serviceTextData = remoteService->textData();

    url->setScheme(protocol);
    if (!userEntry.isNull())
        url->setUserName(QString::fromUtf8(serviceTextData[userEntry]));
    if (!passwordEntry.isNull())
        url->setPassword(QString::fromUtf8(serviceTextData[passwordEntry]));
    if (!pathEntry.isNull())
        url->setPath(QString::fromUtf8(serviceTextData[pathEntry]));

    url->setHost(remoteService->hostName());
    url->setPort(remoteService->port());
}

ZeroConfProtocol::ZeroConfProtocol(const QByteArray &protocol, const QByteArray &pool_socket, const QByteArray &app_socket)
    : WorkerBase(protocol, pool_socket, app_socket)
    , serviceBrowser(nullptr)
    , serviceTypeBrowser(nullptr)
    , serviceToResolve(nullptr)
{
    knownProtocols[QStringLiteral("_ftp._tcp")] =
        ProtocolData(i18n("FTP servers"), QStringLiteral("ftp"), QStringLiteral("path"), QStringLiteral("u"), QStringLiteral("p"));
    knownProtocols[QStringLiteral("_webdav._tcp")] = ProtocolData(i18n("WebDav remote directory"), QStringLiteral("webdav"), QStringLiteral("path"));
    knownProtocols[QStringLiteral("_sftp-ssh._tcp")] =
        ProtocolData(i18n("Remote disk (sftp)"), QStringLiteral("sftp"), QString(), QStringLiteral("u"), QStringLiteral("p"));
    knownProtocols[QStringLiteral("_ssh._tcp")] =
        ProtocolData(i18n("Remote disk (fish)"), QStringLiteral("fish"), QString(), QStringLiteral("u"), QStringLiteral("p"));
    knownProtocols[QStringLiteral("_nfs._tcp")] = ProtocolData(i18n("NFS remote directory"), QStringLiteral("nfs"), QStringLiteral("path"));
}

ZeroConfProtocol::~ZeroConfProtocol()
{
    delete serviceToResolve;
}

KIO::WorkerResult ZeroConfProtocol::get(const QUrl &url)
{
    const KIO::WorkerResult dnssdOkResult = dnssdOK();
    if (!dnssdOkResult.success()) {
        return dnssdOkResult;
    }

    const ZeroConfUrl zeroConfUrl(url);

    ZeroConfUrl::Type type = zeroConfUrl.type();
    if (type != ZeroConfUrl::Service) {
        return KIO::WorkerResult::fail(KIO::ERR_MALFORMED_URL, url.toDisplayString());
    }

    return resolveAndRedirect(zeroConfUrl);
}

KIO::WorkerResult ZeroConfProtocol::mimetype(const QUrl &url)
{
    return resolveAndRedirect(ZeroConfUrl(url));
}

KIO::WorkerResult ZeroConfProtocol::stat(const QUrl &url)
{
    const KIO::WorkerResult dnssdOkResult = dnssdOK();
    if (!dnssdOkResult.success()) {
        return dnssdOkResult;
    }

    const ZeroConfUrl zeroConfUrl(url);

    ZeroConfUrl::Type type = zeroConfUrl.type();

    switch (type) {
    case ZeroConfUrl::RootDir:
    case ZeroConfUrl::ServiceDir: {
        UDSEntry entry;
        feedEntryAsDir(&entry, QString());
        statEntry(entry);
        return KIO::WorkerResult::pass();
    }
    case ZeroConfUrl::Service:
        return resolveAndRedirect(zeroConfUrl);
        break;
    default:
        return KIO::WorkerResult::fail(KIO::ERR_MALFORMED_URL, url.toDisplayString());
    }
}

KIO::WorkerResult ZeroConfProtocol::listDir(const QUrl &url)
{
    const KIO::WorkerResult dnssdOkResult = dnssdOK();
    if (!dnssdOkResult.success()) {
        return dnssdOkResult;
    }

    const ZeroConfUrl zeroConfUrl(url);

    ZeroConfUrl::Type type = zeroConfUrl.type();
    switch (type) {
    case ZeroConfUrl::RootDir:
        listCurrentDirEntry();
        serviceTypeBrowser = new ServiceTypeBrowser(zeroConfUrl.domain());
        connect(serviceTypeBrowser, &ServiceTypeBrowser::serviceTypeAdded, this, &ZeroConfProtocol::addServiceType);
        connect(serviceTypeBrowser, &ServiceTypeBrowser::finished, this, &ZeroConfProtocol::onBrowserFinished);
        serviceTypeBrowser->startBrowse();
        enterLoop();
        return KIO::WorkerResult::pass();
    case ZeroConfUrl::ServiceDir:
        if (!knownProtocols.contains(zeroConfUrl.serviceType())) {
            return KIO::WorkerResult::fail(KIO::ERR_SERVICE_NOT_AVAILABLE, zeroConfUrl.serviceType());
        }
        listCurrentDirEntry();
        serviceBrowser = new ServiceBrowser(zeroConfUrl.serviceType(), false, zeroConfUrl.domain());
        connect(serviceBrowser, &ServiceBrowser::serviceAdded, this, &ZeroConfProtocol::addService);
        connect(serviceBrowser, &ServiceBrowser::finished, this, &ZeroConfProtocol::onBrowserFinished);
        serviceBrowser->startBrowse();
        enterLoop();
        return KIO::WorkerResult::pass();
    case ZeroConfUrl::Service:
        return resolveAndRedirect(zeroConfUrl);
    default:
        return KIO::WorkerResult::fail(KIO::ERR_MALFORMED_URL, url.toDisplayString());
    }
}

KIO::WorkerResult ZeroConfProtocol::dnssdOK()
{
    switch (ServiceBrowser::isAvailable()) {
    case ServiceBrowser::Stopped:
        return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, i18n("The Zeroconf daemon (mdnsd) is not running."));
    case ServiceBrowser::Unsupported:
        return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, i18n("The KDNSSD library has been built without Zeroconf support."));
    default:
        return KIO::WorkerResult::pass();
    }
}

KIO::WorkerResult ZeroConfProtocol::resolveAndRedirect(const ZeroConfUrl &zeroConfUrl)
{
    if (serviceToResolve && !zeroConfUrl.matches(serviceToResolve)) {
        delete serviceToResolve;
        serviceToResolve = nullptr;
    }
    if (serviceToResolve == nullptr) {
        if (!knownProtocols.contains(zeroConfUrl.serviceType())) {
            return KIO::WorkerResult::fail(KIO::ERR_SERVICE_NOT_AVAILABLE, zeroConfUrl.serviceType());
        }

        serviceToResolve = new RemoteService(zeroConfUrl.serviceName(), zeroConfUrl.serviceType(), zeroConfUrl.domain());
        if (!serviceToResolve->resolve()) {
            return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, zeroConfUrl.serviceName());
        }
    }

    if (!knownProtocols.contains(zeroConfUrl.serviceType()))
        return KIO::WorkerResult::fail();

    // action
    const ProtocolData &protocolData = knownProtocols[zeroConfUrl.serviceType()];
    QUrl destUrl;
    protocolData.feedUrl(&destUrl, serviceToResolve);

    redirection(destUrl);

    return KIO::WorkerResult::pass();
}

void ZeroConfProtocol::listCurrentDirEntry()
{
    UDSEntry entry;
    feedEntryAsDir(&entry, QStringLiteral("."));
    listEntry(entry);
}

void ZeroConfProtocol::addServiceType(const QString &serviceType)
{
    if (ServiceTypesAdded.contains(serviceType))
        return;
    ServiceTypesAdded << serviceType;

    if (!knownProtocols.contains(serviceType))
        return;

    // action
    UDSEntry entry;
    feedEntryAsDir(&entry, serviceType, knownProtocols[serviceType].name);
    listEntry(entry);
}

void ZeroConfProtocol::addService(KDNSSD::RemoteService::Ptr service)
{
    UDSEntry entry;
    entry.fastInsert(UDSEntry::UDS_NAME, service->serviceName());
    entry.fastInsert(UDSEntry::UDS_ACCESS, 0666);
    entry.fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFDIR);
    const QString iconName = KProtocolInfo::icon(knownProtocols[service->type()].protocol);
    if (!iconName.isNull())
        entry.fastInsert(UDSEntry::UDS_ICON_NAME, iconName);

    listEntry(entry);
}

void ZeroConfProtocol::onBrowserFinished()
{
    // cleanup
    if (serviceBrowser) {
        serviceBrowser->deleteLater();
        serviceBrowser = nullptr;
    }
    if (serviceTypeBrowser) {
        serviceTypeBrowser->deleteLater();
        serviceTypeBrowser = nullptr;
    }
    ServiceTypesAdded.clear();

    Q_EMIT leaveModality();
}

void ZeroConfProtocol::feedEntryAsDir(UDSEntry *entry, const QString &name, const QString &displayName)
{
    entry->fastInsert(UDSEntry::UDS_NAME, name);
    entry->fastInsert(UDSEntry::UDS_ACCESS, 0555);
    entry->fastInsert(UDSEntry::UDS_FILE_TYPE, S_IFDIR);
    entry->fastInsert(UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
    if (!displayName.isEmpty())
        entry->fastInsert(UDSEntry::UDS_DISPLAY_NAME, displayName);
}

void ZeroConfProtocol::enterLoop()
{
    QEventLoop eventLoop;
    connect(this, &ZeroConfProtocol::leaveModality, &eventLoop, &QEventLoop::quit);
    eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
}

extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv)
{
    // necessary to use other KIO workers
    QCoreApplication app(argc, argv);
    app.setApplicationName(QStringLiteral("kio_zeroconf"));

    if (argc != 4) {
        fprintf(stderr, "Usage: %s protocol domain-socket1 domain-socket2\n", argv[0]);
        exit(-1);
    }

    // start the worker
    ZeroConfProtocol worker(argv[1], argv[2], argv[3]);
    worker.dispatchLoop();
    return 0;
}

#include "dnssd.moc"
#include "moc_dnssd.cpp"
