/*
    This file is part of the KDE libraries
    SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
    SPDX-FileCopyrightText: 2000-2009 David Faure <faure@kde.org>

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

#include "listjob.h"
#include "../utils_p.h"
#include "job_p.h"
#include "worker_p.h"
#include <QTimer>
#include <kurlauthorized.h>

#include <QDebug>

using namespace KIO;

class KIO::ListJobPrivate : public KIO::SimpleJobPrivate
{
public:
    ListJobPrivate(const QUrl &url, bool _recursive, const QString &prefix, const QString &displayPrefix, ListJob::ListFlags listFlags)
        : SimpleJobPrivate(url, CMD_LISTDIR, QByteArray())
        , recursive(_recursive)
        , listFlags(listFlags)
        , m_prefix(prefix)
        , m_displayPrefix(displayPrefix)
        , m_processedEntries(0)
    {
    }
    bool recursive;
    ListJob::ListFlags listFlags;
    QString m_prefix;
    QString m_displayPrefix;
    unsigned long m_processedEntries;
    QUrl m_redirectionURL;

    /*!
     * \internal
     * Called by the scheduler when a @p worker gets to
     * work on this job.
     * \a worker the worker that starts working on this job
     */
    void start(Worker *worker) override;

    void filterAndEmitEntries(const KIO::UDSEntryList &list);
    void maybeRecurse(const KIO::UDSEntryList &list);
    void slotRedirection(const QUrl &url);
    void gotEntries(KIO::Job *subjob, const KIO::UDSEntryList &list);
    void slotSubError(ListJob *job, ListJob *subJob);

    Q_DECLARE_PUBLIC(ListJob)

    static inline ListJob *
    newJob(const QUrl &u, bool _recursive, const QString &prefix, const QString &displayPrefix, ListJob::ListFlags listFlags, JobFlags flags = HideProgressInfo)
    {
        ListJob *job = new ListJob(*new ListJobPrivate(u, _recursive, prefix, displayPrefix, listFlags));
        job->setUiDelegate(KIO::createDefaultJobUiDelegate());
        if (!(flags & HideProgressInfo)) {
            KIO::getJobTracker()->registerJob(job);
        }
        return job;
    }
    static inline ListJob *newJobNoUi(const QUrl &u, bool _recursive, const QString &prefix, const QString &displayPrefix, ListJob::ListFlags listFlags)
    {
        return new ListJob(*new ListJobPrivate(u, _recursive, prefix, displayPrefix, listFlags));
    }
};

ListJob::ListJob(ListJobPrivate &dd)
    : SimpleJob(dd)
{
    Q_D(ListJob);
    // We couldn't set the args when calling the parent constructor,
    // so do it now.
    QDataStream stream(&d->m_packedArgs, QIODevice::WriteOnly);
    stream << d->m_url;
}

ListJob::~ListJob()
{
}

void ListJobPrivate::maybeRecurse(const KIO::UDSEntryList &list)
{
    Q_Q(ListJob);

    const bool includeHidden = listFlags.testFlag(ListJob::ListFlag::IncludeHidden);

    if (!recursive) {
        return;
    }

    for (const KIO::UDSEntry &entry : list) {
        QUrl itemURL;
        const QString udsUrl = entry.stringValue(KIO::UDSEntry::UDS_URL);
        QString filename;
        if (!udsUrl.isEmpty()) {
            itemURL = QUrl(udsUrl);
            filename = itemURL.fileName();
        } else { // no URL, use the name
            itemURL = q->url();
            filename = entry.stringValue(KIO::UDSEntry::UDS_NAME);
            Q_ASSERT(!filename.isEmpty()); // we'll recurse forever otherwise :)
            itemURL.setPath(Utils::concatPaths(itemURL.path(), filename));
        }

        if (!entry.isDir() || entry.isLink()) {
            continue;
        }

        Q_ASSERT(!filename.isEmpty());
        QString displayName = entry.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME);
        if (displayName.isEmpty()) {
            displayName = filename;
        }
        // skip hidden dirs when listing if requested
        if (filename != QLatin1String("..") && filename != QLatin1String(".") && (includeHidden || filename[0] != QLatin1Char('.'))) {
            ListJob *job = ListJobPrivate::newJobNoUi(itemURL,
                                                      true /*recursive*/,
                                                      m_prefix + filename + QLatin1Char('/'),
                                                      m_displayPrefix + displayName + QLatin1Char('/'),
                                                      listFlags);
            QObject::connect(job, &ListJob::entries, q, [this](KIO::Job *job, const KIO::UDSEntryList &list) {
                gotEntries(job, list);
            });
            QObject::connect(job, &ListJob::subError, q, [this](KIO::ListJob *job, KIO::ListJob *ljob) {
                slotSubError(job, ljob);
            });

            q->addSubjob(job);
        }
    }
}

void ListJobPrivate::filterAndEmitEntries(const KIO::UDSEntryList &list)
{
    Q_Q(ListJob);

    const bool includeHidden = listFlags.testFlag(ListJob::ListFlag::IncludeHidden);
    const bool excludeDot = listFlags.testFlag(ListJob::ListFlag::ExcludeDot);
    const bool excludeDotDot = listFlags.testFlag(ListJob::ListFlag::ExcludeDotDot);

    // Emit progress info (takes care of emit processedSize and percent)
    m_processedEntries += list.count();
    slotProcessedSize(m_processedEntries);

    // Not recursive, or top-level of recursive listing : return now (send . and .. as well)
    // exclusion of hidden files also requires the full sweep, but the case for full-listing
    // a single dir is probably common enough to justify the shortcut
    if (m_prefix.isNull() && includeHidden && !excludeDot && !excludeDotDot) {
        Q_EMIT q->entries(q, list);
    } else {
        UDSEntryList newlist = list;

        // Mind that the repeated iteration hardly makes a performance difference here, presumably
        // because the compiler is smart enough to optimize it away.
        // Same for repeat calls to stringValue().
        // Benchmark before you decide to make improvements here!

        auto removeFunc = [this, includeHidden, excludeDot, excludeDotDot](const UDSEntry &entry) {
            const QString filename = entry.stringValue(KIO::UDSEntry::UDS_NAME);

            // Avoid returning entries like subdir/. and subdir/.., but include . and .. for
            // the toplevel dir unless excludeDot/excludeDotDot were passed
            if (m_prefix.isNull()) {
                if ((excludeDotDot && filename == QLatin1String("..")) || (excludeDot && filename == QLatin1String("."))) {
                    return true;
                }
            } else {
                if (filename == QLatin1String("..") || filename == QLatin1String(".")) {
                    return true;
                }
            }

            // skip hidden files/dirs if that was requested
            if (!includeHidden && filename[0] == QLatin1Char('.')) {
                return true;
            }

            return false;
        };
        newlist.erase(std::remove_if(newlist.begin(), newlist.end(), removeFunc), newlist.end());

        for (UDSEntry &newone : newlist) {
            // Modify the name in the UDSEntry
            const QString filename = newone.stringValue(KIO::UDSEntry::UDS_NAME);
            QString displayName = newone.stringValue(KIO::UDSEntry::UDS_DISPLAY_NAME);
            if (displayName.isEmpty()) {
                displayName = filename;
            }

            newone.replace(KIO::UDSEntry::UDS_NAME, m_prefix + filename);
            newone.replace(KIO::UDSEntry::UDS_DISPLAY_NAME, m_displayPrefix + displayName);
        }

        Q_EMIT q->entries(q, newlist);
    }
}

void ListJobPrivate::gotEntries(KIO::Job *, const KIO::UDSEntryList &list)
{
    // Forward entries received by subjob - faking we received them ourselves
    Q_Q(ListJob);
    Q_EMIT q->entries(q, list);
}

void ListJobPrivate::slotSubError(KIO::ListJob * /*job*/, KIO::ListJob *subJob)
{
    Q_Q(ListJob);
    Q_EMIT q->subError(q, subJob); // Let the signal of subError go up
}

void ListJob::slotResult(KJob *job)
{
    Q_D(ListJob);
    if (job->error()) {
        // If we can't list a subdir, the result is still ok
        // This is why we override KCompositeJob::slotResult - to not set
        // an error on parent job.
        // Let's emit a signal about this though
        Q_EMIT subError(this, static_cast<KIO::ListJob *>(job));
    }
    removeSubjob(job);
    if (!hasSubjobs() && !d->m_worker) { // if the main directory listing is still running, it will emit result in SimpleJob::slotFinished()
        emitResult();
    }
}

void ListJobPrivate::slotRedirection(const QUrl &url)
{
    Q_Q(ListJob);
    if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("redirect"), m_url, url)) {
        qCWarning(KIO_CORE) << "Redirection from" << m_url << "to" << url << "REJECTED!";
        return;
    }
    m_redirectionURL = url; // We'll remember that when the job finishes
    Q_EMIT q->redirection(q, m_redirectionURL);
}

void ListJob::slotFinished()
{
    Q_D(ListJob);

    if (!d->m_redirectionURL.isEmpty() && d->m_redirectionURL.isValid() && !error()) {
        // qDebug() << "Redirection to " << d->m_redirectionURL;
        if (queryMetaData(QStringLiteral("permanent-redirect")) == QLatin1String("true")) {
            Q_EMIT permanentRedirection(this, d->m_url, d->m_redirectionURL);
        }

        if (d->m_redirectionHandlingEnabled) {
            d->m_packedArgs.truncate(0);
            QDataStream stream(&d->m_packedArgs, QIODevice::WriteOnly);
            stream << d->m_redirectionURL;

            d->restartAfterRedirection(&d->m_redirectionURL);
            return;
        }
    }

    // Return worker to the scheduler
    SimpleJob::slotFinished();
}

ListJob *KIO::listDir(const QUrl &url, JobFlags flags, ListJob::ListFlags listFlags)
{
    return ListJobPrivate::newJob(url, false, QString(), QString(), listFlags, flags);
}

ListJob *KIO::listRecursive(const QUrl &url, JobFlags flags, ListJob::ListFlags listFlags)
{
    return ListJobPrivate::newJob(url, true, QString(), QString(), listFlags, flags);
}

void ListJob::setUnrestricted(bool unrestricted)
{
    Q_D(ListJob);
    if (unrestricted) {
        d->m_extraFlags |= JobPrivate::EF_ListJobUnrestricted;
    } else {
        d->m_extraFlags &= ~JobPrivate::EF_ListJobUnrestricted;
    }
}

void ListJobPrivate::start(Worker *worker)
{
    Q_Q(ListJob);
    if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("list"), m_url, m_url) && !(m_extraFlags & EF_ListJobUnrestricted)) {
        q->setError(ERR_ACCESS_DENIED);
        q->setErrorText(m_url.toDisplayString());
        QTimer::singleShot(0, q, &ListJob::slotFinished);
        return;
    }
    QObject::connect(worker, &Worker::listEntries, q, [this](KIO::UDSEntryList list) {
        maybeRecurse(list);
        filterAndEmitEntries(list);
    });

    QObject::connect(worker, &Worker::totalSize, q, [this](KIO::filesize_t size) {
        slotTotalSize(size);
    });

    QObject::connect(worker, &Worker::redirection, q, [this](const QUrl &url) {
        slotRedirection(url);
    });

    SimpleJobPrivate::start(worker);
}

const QUrl &ListJob::redirectionUrl() const
{
    return d_func()->m_redirectionURL;
}

#include "moc_listjob.cpp"
