/*
    This file is part of the KDE project
    SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
    SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
    SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>

    SPDX-License-Identifier: LGPL-2.0-only
*/

#include "kfileplacesmodel.h"
#include "kfileplacesitem_p.h"
#include "kfileplacesmodel_p.h"

#include <KCoreDirLister>
#include <KLazyLocalizedString>
#include <KListOpenFilesJob>
#include <KLocalizedString>
#include <commandlauncherjob.h>
#include <kfileitem.h>
#include <kio/statjob.h>
#include <kprotocolinfo.h>

#include <KBookmarkManager>
#include <KConfig>
#include <KConfigGroup>
#include <KUrlMimeData>

#include <solid/block.h>
#include <solid/devicenotifier.h>
#include <solid/opticaldisc.h>
#include <solid/opticaldrive.h>
#include <solid/portablemediaplayer.h>
#include <solid/predicate.h>
#include <solid/storageaccess.h>
#include <solid/storagedrive.h>
#include <solid/storagevolume.h>

#include <QAction>
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QMimeData>
#include <QMimeDatabase>
#include <QStandardPaths>
#include <QTimer>

namespace
{
QString stateNameForGroupType(KFilePlacesModel::GroupType type)
{
    switch (type) {
    case KFilePlacesModel::PlacesType:
        return QStringLiteral("GroupState-Places-IsHidden");
    case KFilePlacesModel::RemoteType:
        return QStringLiteral("GroupState-Remote-IsHidden");
    case KFilePlacesModel::RecentlySavedType:
        return QStringLiteral("GroupState-RecentlySaved-IsHidden");
    case KFilePlacesModel::SearchForType:
        return QStringLiteral("GroupState-SearchFor-IsHidden");
    case KFilePlacesModel::DevicesType:
        return QStringLiteral("GroupState-Devices-IsHidden");
    case KFilePlacesModel::RemovableDevicesType:
        return QStringLiteral("GroupState-RemovableDevices-IsHidden");
    case KFilePlacesModel::TagsType:
        return QStringLiteral("GroupState-Tags-IsHidden");
    default:
        Q_UNREACHABLE();
    }
}

static bool isFileIndexingEnabled()
{
    KConfig config(QStringLiteral("baloofilerc"));
    KConfigGroup basicSettings = config.group(QStringLiteral("Basic Settings"));
    return basicSettings.readEntry("Indexing-Enabled", true);
}

static QString timelineDateString(int year, int month, int day = 0)
{
    const QString dateFormat = QStringLiteral("%1-%2");

    QString date = dateFormat.arg(year).arg(month, 2, 10, QLatin1Char('0'));
    if (day > 0) {
        date += QStringLiteral("-%1").arg(day, 2, 10, QLatin1Char('0'));
    }
    return date;
}

static QUrl createTimelineUrl(const QUrl &url)
{
    // based on dolphin urls
    const QString timelinePrefix = QLatin1String("timeline:") + QLatin1Char('/');
    QUrl timelineUrl;

    const QString path = url.toDisplayString(QUrl::PreferLocalFile);
    if (path.endsWith(QLatin1String("/yesterday"))) {
        const QDate date = QDate::currentDate().addDays(-1);
        const int year = date.year();
        const int month = date.month();
        const int day = date.day();

        timelineUrl = QUrl(timelinePrefix + timelineDateString(year, month) + QLatin1Char('/') + timelineDateString(year, month, day));
    } else if (path.endsWith(QLatin1String("/thismonth"))) {
        const QDate date = QDate::currentDate();
        timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month()));
    } else if (path.endsWith(QLatin1String("/lastmonth"))) {
        const QDate date = QDate::currentDate().addMonths(-1);
        timelineUrl = QUrl(timelinePrefix + timelineDateString(date.year(), date.month()));
    } else {
        timelineUrl = url;
    }

    return timelineUrl;
}

static QUrl createSearchUrl(const QUrl &url)
{
    QUrl searchUrl = url;

    const QString path = url.toDisplayString(QUrl::PreferLocalFile);

    const QStringList validSearchPaths = {QStringLiteral("/documents"), QStringLiteral("/images"), QStringLiteral("/audio"), QStringLiteral("/videos")};

    for (const QString &validPath : validSearchPaths) {
        if (path.endsWith(validPath)) {
            searchUrl.setScheme(QStringLiteral("baloosearch"));
            return searchUrl;
        }
    }

    qWarning() << "Invalid search url:" << url;

    return searchUrl;
}
}

KFilePlacesModelPrivate::KFilePlacesModelPrivate(KFilePlacesModel *qq)
    : q(qq)
    , fileIndexingEnabled(isFileIndexingEnabled())
    , tagsLister(new KCoreDirLister(q))
{
    if (KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) {
        QObject::connect(tagsLister, &KCoreDirLister::itemsAdded, q, [this](const QUrl &, const KFileItemList &items) {
            if (tags.isEmpty()) {
                QList<QUrl> existingBookmarks;

                KBookmarkGroup root = bookmarkManager->root();
                KBookmark bookmark = root.first();

                while (!bookmark.isNull()) {
                    existingBookmarks.append(bookmark.url());
                    bookmark = root.next(bookmark);
                }

                if (!existingBookmarks.contains(QUrl(tagsUrlBase))) {
                    KBookmark alltags = KFilePlacesItem::createSystemBookmark(bookmarkManager,
                                                                              kli18nc("KFile System Bookmarks", "All tags").untranslatedText(),
                                                                              QUrl(tagsUrlBase),
                                                                              QStringLiteral("tag"));
                }
            }

            for (const KFileItem &item : items) {
                const QString name = item.name();

                if (!tags.contains(name)) {
                    tags.append(name);
                }
            }
            reloadBookmarks();
        });

        QObject::connect(tagsLister, &KCoreDirLister::itemsDeleted, q, [this](const KFileItemList &items) {
            for (const KFileItem &item : items) {
                tags.removeAll(item.name());
            }
            reloadBookmarks();
        });

        tagsLister->openUrl(QUrl(tagsUrlBase), KCoreDirLister::OpenUrlFlag::Reload);
    }
}

QString KFilePlacesModelPrivate::ignoreMimeType()
{
    return QStringLiteral("application/x-kfileplacesmodel-ignore");
}

QString KFilePlacesModelPrivate::internalMimeType(const KFilePlacesModel *model)
{
    return QStringLiteral("application/x-kfileplacesmodel-") + QString::number(reinterpret_cast<qptrdiff>(model));
}

KBookmark KFilePlacesModel::bookmarkForUrl(const QUrl &searchUrl) const
{
    KBookmarkGroup root = d->bookmarkManager->root();
    KBookmark current = root.first();
    while (!current.isNull()) {
        if (current.url() == searchUrl) {
            return current;
        }
        current = root.next(current);
    }
    return KBookmark();
}

static inline QString versionKey()
{
    return QStringLiteral("kde_places_version");
}

KFilePlacesModel::KFilePlacesModel(QObject *parent)
    : QAbstractItemModel(parent)
    , d(new KFilePlacesModelPrivate(this))
{
    const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/user-places.xbel");
    d->bookmarkManager = new KBookmarkManager(file, this);

    // Let's put some places in there if it's empty.
    KBookmarkGroup root = d->bookmarkManager->root();

    const auto setDefaultMetadataItemForGroup = [&root](KFilePlacesModel::GroupType type) {
        root.setMetaDataItem(stateNameForGroupType(type), QStringLiteral("false"));
    };

    // Increase this version number and use the following logic to handle the update process for existing installations.
    static const int s_currentVersion = 4;

    const bool newFile = root.first().isNull() || !QFile::exists(file);
    const int fileVersion = root.metaDataItem(versionKey()).toInt();

    if (newFile || fileVersion < s_currentVersion) {
        root.setMetaDataItem(versionKey(), QString::number(s_currentVersion));

        const QList<QUrl> seenUrls = root.groupUrlList();

        /* clang-format off */
        auto createSystemBookmark =
            [this, &seenUrls](const char *untranslatedLabel,
                              const QUrl &url,
                              const QString &iconName,
                              const KBookmark &after) {
                if (!seenUrls.contains(url)) {
                    return KFilePlacesItem::createSystemBookmark(d->bookmarkManager, untranslatedLabel, url, iconName, after);
                }
                return KBookmark();
            };
        /* clang-format on */

        if (fileVersion < 2) {
            // NOTE: The context for these kli18nc calls has to be "KFile System Bookmarks".
            // The real i18nc call is made later, with this context, so the two must match.
            createSystemBookmark(kli18nc("KFile System Bookmarks", "Home").untranslatedText(),
                                 QUrl::fromLocalFile(QDir::homePath()),
                                 QStringLiteral("user-home"),
                                 KBookmark());

            // Some distros may not create various standard XDG folders by default
            // so check for their existence before adding bookmarks for them
            const QString desktopFolder = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
            if (QDir(desktopFolder).exists()) {
                createSystemBookmark(kli18nc("KFile System Bookmarks", "Desktop").untranslatedText(),
                                     QUrl::fromLocalFile(desktopFolder),
                                     QStringLiteral("user-desktop"),
                                     KBookmark());
            }
            const QString documentsFolder = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
            if (QDir(documentsFolder).exists()) {
                createSystemBookmark(kli18nc("KFile System Bookmarks", "Documents").untranslatedText(),
                                     QUrl::fromLocalFile(documentsFolder),
                                     QStringLiteral("folder-documents"),
                                     KBookmark());
            }
            const QString downloadFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
            if (QDir(downloadFolder).exists()) {
                createSystemBookmark(kli18nc("KFile System Bookmarks", "Downloads").untranslatedText(),
                                     QUrl::fromLocalFile(downloadFolder),
                                     QStringLiteral("folder-downloads"),
                                     KBookmark());
            }
            createSystemBookmark(kli18nc("KFile System Bookmarks", "Network").untranslatedText(),
                                 QUrl(QStringLiteral("remote:/")),
                                 QStringLiteral("folder-network"),
                                 KBookmark());

            createSystemBookmark(kli18nc("KFile System Bookmarks", "Trash").untranslatedText(),
                                 QUrl(QStringLiteral("trash:/")),
                                 QStringLiteral("user-trash"),
                                 KBookmark());
        }

        if (!newFile && fileVersion < 3) {
            KBookmarkGroup rootGroup = d->bookmarkManager->root();
            KBookmark bItem = rootGroup.first();
            while (!bItem.isNull()) {
                KBookmark nextbItem = rootGroup.next(bItem);
                const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true");
                if (isSystemItem) {
                    const QString text = bItem.fullText();
                    // Because of b8a4c2223453932202397d812a0c6b30c6186c70 we need to find the system bookmark named Audio Files
                    // and rename it to Audio, otherwise users are getting untranslated strings
                    if (text == QLatin1String("Audio Files")) {
                        bItem.setFullText(QStringLiteral("Audio"));
                    } else if (text == QLatin1String("Today")) {
                        // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Today
                        // and rename it to Modified Today, otherwise users are getting untranslated strings
                        bItem.setFullText(QStringLiteral("Modified Today"));
                    } else if (text == QLatin1String("Yesterday")) {
                        // Because of 19feef732085b444515da3f6c66f3352bbcb1824 we need to find the system bookmark named Yesterday
                        // and rename it to Modified Yesterday, otherwise users are getting untranslated strings
                        bItem.setFullText(QStringLiteral("Modified Yesterday"));
                    } else if (text == QLatin1String("This Month")) {
                        // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named This Month
                        // and remove it, otherwise users are getting untranslated strings
                        rootGroup.deleteBookmark(bItem);
                    } else if (text == QLatin1String("Last Month")) {
                        // Because of 7e1d2fb84546506c91684dd222c2485f0783848f we need to find the system bookmark named Last Month
                        // and remove it, otherwise users are getting untranslated strings
                        rootGroup.deleteBookmark(bItem);
                    }
                }

                bItem = nextbItem;
            }
        }
        if (fileVersion < 4) {
            auto findSystemBookmark = [this](const QString &untranslatedText) {
                KBookmarkGroup root = d->bookmarkManager->root();
                KBookmark bItem = root.first();
                while (!bItem.isNull()) {
                    const bool isSystemItem = bItem.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true");
                    if (isSystemItem && bItem.fullText() == untranslatedText) {
                        return bItem;
                    }
                    bItem = root.next(bItem);
                }
                return KBookmark();
            };
            // This variable is used to insert the new bookmarks at the correct place starting after the "Downloads"
            // bookmark. When the user already has some of the bookmarks set up manually, the createSystemBookmark()
            // function returns an empty KBookmark so the following entries will be added at the end of the bookmark
            // section to not mess with the users setup.
            KBookmark after = findSystemBookmark(QLatin1String("Downloads"));

            const QString musicFolder = QStandardPaths::writableLocation(QStandardPaths::MusicLocation);
            if (QDir(musicFolder).exists()) {
                after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Music").untranslatedText(),
                                             QUrl::fromLocalFile(musicFolder),
                                             QStringLiteral("folder-music"),
                                             after);
            }
            const QString pictureFolder = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
            if (QDir(pictureFolder).exists()) {
                after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Pictures").untranslatedText(),
                                             QUrl::fromLocalFile(pictureFolder),
                                             QStringLiteral("folder-pictures"),
                                             after);
            }
            // Choosing the name "Videos" instead of "Movies", since that is how the folder
            // is called normally on Linux: https://cgit.freedesktop.org/xdg/xdg-user-dirs/tree/user-dirs.defaults
            const QString videoFolder = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
            if (QDir(videoFolder).exists()) {
                after = createSystemBookmark(kli18nc("KFile System Bookmarks", "Videos").untranslatedText(),
                                             QUrl::fromLocalFile(videoFolder),
                                             QStringLiteral("folder-videos"),
                                             after);
            }
        }

        if (newFile) {
            setDefaultMetadataItemForGroup(PlacesType);
            setDefaultMetadataItemForGroup(RemoteType);
            setDefaultMetadataItemForGroup(DevicesType);
            setDefaultMetadataItemForGroup(RemovableDevicesType);
            setDefaultMetadataItemForGroup(TagsType);
        }

        // Force bookmarks to be saved. If on open/save dialog and the bookmarks are not saved, QFile::exists
        // will always return false, which opening/closing all the time the open/save dialog would cause the
        // bookmarks to be added once each time, having lots of times each bookmark. (ereslibre)
        d->bookmarkManager->saveAs(file);
    }

    // Add a Recently Used entry if available (it comes from kio-extras)
    if (qEnvironmentVariableIsSet("KDE_FULL_SESSION") && KProtocolInfo::isKnownProtocol(QStringLiteral("recentlyused"))
        && root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) {
        root.setMetaDataItem(QStringLiteral("withRecentlyUsed"), QStringLiteral("true"));

        KBookmark recentFilesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
                                                                              kli18nc("KFile System Bookmarks", "Recent Files").untranslatedText(),
                                                                              QUrl(QStringLiteral("recentlyused:/files")),
                                                                              QStringLiteral("document-open-recent"));

        KBookmark recentDirectoriesBookmark = KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
                                                                                    kli18nc("KFile System Bookmarks", "Recent Locations").untranslatedText(),
                                                                                    QUrl(QStringLiteral("recentlyused:/locations")),
                                                                                    QStringLiteral("folder-open-recent"));

        setDefaultMetadataItemForGroup(RecentlySavedType);

        // Move The recently used bookmarks below the trash, making it the first element in the Recent group
        KBookmark trashBookmark = bookmarkForUrl(QUrl(QStringLiteral("trash:/")));
        if (!trashBookmark.isNull()) {
            root.moveBookmark(recentFilesBookmark, trashBookmark);
            root.moveBookmark(recentDirectoriesBookmark, recentFilesBookmark);
        }

        d->bookmarkManager->save();
    }

    // if baloo is enabled, add new urls even if the bookmark file is not empty
    if (d->fileIndexingEnabled && root.metaDataItem(QStringLiteral("withBaloo")) != QLatin1String("true")) {
        root.setMetaDataItem(QStringLiteral("withBaloo"), QStringLiteral("true"));

        // don't add by default "Modified Today" and "Modified Yesterday" when recentlyused:/ is present
        if (root.metaDataItem(QStringLiteral("withRecentlyUsed")) != QLatin1String("true")) {
            KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
                                                  kli18nc("KFile System Bookmarks", "Modified Today").untranslatedText(),
                                                  QUrl(QStringLiteral("timeline:/today")),
                                                  QStringLiteral("go-jump-today"));
            KFilePlacesItem::createSystemBookmark(d->bookmarkManager,
                                                  kli18nc("KFile System Bookmarks", "Modified Yesterday").untranslatedText(),
                                                  QUrl(QStringLiteral("timeline:/yesterday")),
                                                  QStringLiteral("view-calendar-day"));
        }

        setDefaultMetadataItemForGroup(SearchForType);
        setDefaultMetadataItemForGroup(RecentlySavedType);

        d->bookmarkManager->save();
    }

    QString predicate(
        QString::fromLatin1("[[[[ StorageVolume.ignored == false AND [ StorageVolume.usage == 'FileSystem' OR StorageVolume.usage == 'Encrypted' ]]"
                            " OR "
                            "[ IS StorageAccess AND StorageDrive.driveType == 'Floppy' ]]"
                            " OR "
                            "OpticalDisc.availableContent & 'Audio' ]"
                            " OR "
                            "StorageAccess.ignored == false ]"));

    if (KProtocolInfo::isKnownProtocol(QStringLiteral("mtp"))) {
        predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'mtp']");
    }
    if (KProtocolInfo::isKnownProtocol(QStringLiteral("afc"))) {
        predicate = QLatin1Char('[') + predicate + QLatin1String(" OR PortableMediaPlayer.supportedProtocols == 'afc']");
    }

    d->predicate = Solid::Predicate::fromString(predicate);

    Q_ASSERT(d->predicate.isValid());

    connect(d->bookmarkManager, &KBookmarkManager::changed, this, [this]() {
        d->reloadBookmarks();
    });

    d->reloadBookmarks();
    QTimer::singleShot(0, this, [this]() {
        d->initDeviceList();
    });
}

KFilePlacesModel::~KFilePlacesModel() = default;

QUrl KFilePlacesModel::url(const QModelIndex &index) const
{
    return data(index, UrlRole).toUrl();
}

bool KFilePlacesModel::setupNeeded(const QModelIndex &index) const
{
    return data(index, SetupNeededRole).toBool();
}

QIcon KFilePlacesModel::icon(const QModelIndex &index) const
{
    return data(index, Qt::DecorationRole).value<QIcon>();
}

QString KFilePlacesModel::text(const QModelIndex &index) const
{
    return data(index, Qt::DisplayRole).toString();
}

bool KFilePlacesModel::isHidden(const QModelIndex &index) const
{
    // Note: we do not want to show an index if its parent is hidden
    return data(index, HiddenRole).toBool() || isGroupHidden(index);
}

bool KFilePlacesModel::isGroupHidden(const GroupType type) const
{
    const QString hidden = d->bookmarkManager->root().metaDataItem(stateNameForGroupType(type));
    return hidden == QLatin1String("true");
}

bool KFilePlacesModel::isGroupHidden(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return false;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
    return isGroupHidden(item->groupType());
}

bool KFilePlacesModel::isDevice(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return false;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());

    return item->isDevice();
}

bool KFilePlacesModel::isTeardownAllowed(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return false;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
    return item->isTeardownAllowed();
}

bool KFilePlacesModel::isEjectAllowed(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return false;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
    return item->isEjectAllowed();
}

bool KFilePlacesModel::isTeardownOverlayRecommended(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return false;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
    return item->isTeardownOverlayRecommended();
}

KFilePlacesModel::DeviceAccessibility KFilePlacesModel::deviceAccessibility(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return KFilePlacesModel::Accessible;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
    return item->deviceAccessibility();
}

Solid::Device KFilePlacesModel::deviceForIndex(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return Solid::Device();
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());

    if (item->isDevice()) {
        return item->device();
    } else {
        return Solid::Device();
    }
}

KBookmark KFilePlacesModel::bookmarkForIndex(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return KBookmark();
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
    return item->bookmark();
}

KFilePlacesModel::GroupType KFilePlacesModel::groupType(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return UnknownType;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
    return item->groupType();
}

QModelIndexList KFilePlacesModel::groupIndexes(const KFilePlacesModel::GroupType type) const
{
    if (type == UnknownType) {
        return QModelIndexList();
    }

    QModelIndexList indexes;
    const int rows = rowCount();
    for (int row = 0; row < rows; ++row) {
        const QModelIndex current = index(row, 0);
        if (groupType(current) == type) {
            indexes << current;
        }
    }

    return indexes;
}

QVariant KFilePlacesModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) {
        return QVariant();
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());
    if (role == KFilePlacesModel::GroupHiddenRole) {
        return isGroupHidden(item->groupType());
    } else {
        return item->data(role);
    }
}

QModelIndex KFilePlacesModel::index(int row, int column, const QModelIndex &parent) const
{
    if (row < 0 || column != 0 || row >= d->items.size()) {
        return QModelIndex();
    }

    if (parent.isValid()) {
        return QModelIndex();
    }

    return createIndex(row, column, d->items.at(row));
}

QModelIndex KFilePlacesModel::parent(const QModelIndex &child) const
{
    Q_UNUSED(child);
    return QModelIndex();
}

QHash<int, QByteArray> KFilePlacesModel::roleNames() const
{
    auto super = QAbstractItemModel::roleNames();

    super[UrlRole] = "url";
    super[HiddenRole] = "isHidden";
    super[SetupNeededRole] = "isSetupNeeded";
    super[FixedDeviceRole] = "isFixedDevice";
    super[CapacityBarRecommendedRole] = "isCapacityBarRecommended";
    super[GroupRole] = "group";
    super[IconNameRole] = "iconName";
    super[GroupHiddenRole] = "isGroupHidden";
    super[TeardownAllowedRole] = "isTeardownAllowed";
    super[EjectAllowedRole] = "isEjectAllowed";
    super[TeardownOverlayRecommendedRole] = "isTeardownOverlayRecommended";
    super[DeviceAccessibilityRole] = "deviceAccessibility";

    return super;
}

int KFilePlacesModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid()) {
        return 0;
    } else {
        return d->items.size();
    }
}

int KFilePlacesModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent)
    // We only know 1 piece of information for a particular entry
    return 1;
}

QModelIndex KFilePlacesModel::closestItem(const QUrl &url) const
{
    int foundRow = -1;
    int maxLength = 0;

    // Search the item which is equal to the URL or at least is a parent URL.
    // If there are more than one possible item URL candidates, choose the item
    // which covers the bigger range of the URL.
    for (int row = 0; row < d->items.size(); ++row) {
        KFilePlacesItem *item = d->items[row];

        if (item->isHidden() || isGroupHidden(item->groupType())) {
            continue;
        }

        const QUrl itemUrl = convertedUrl(item->data(UrlRole).toUrl());

        if (itemUrl.matches(url, QUrl::StripTrailingSlash)
            || (itemUrl.isParentOf(url) && itemUrl.query() == url.query() && itemUrl.fragment() == url.fragment())) {
            const int length = itemUrl.toString().length();
            if (length > maxLength) {
                foundRow = row;
                maxLength = length;
            }
        }
    }

    if (foundRow == -1) {
        return QModelIndex();
    } else {
        return createIndex(foundRow, 0, d->items[foundRow]);
    }
}

QString KFilePlacesModel::placeCollapsedUrl(const QUrl &_url) const
{
    // Mimic KUrlNavigator and show a pretty place name,
    // for example Documents/foo/bar rather than /home/user/Documents/foo/bar
    const QModelIndex closestIdx = closestItem(_url);
    if (!closestIdx.isValid()) {
        return QString();
    }

    const QUrl placeUrl = url(closestIdx);

    QString result = text(closestIdx);
    // Guard again erroneous user-places.xbel file
    if (result.isEmpty()) {
        return QString();
    }

    QString pathInsidePlace = _url.path().mid(placeUrl.path().length());

    if (!pathInsidePlace.startsWith(QLatin1Char('/'))) {
        pathInsidePlace.prepend(QLatin1Char('/'));
    }

    if (pathInsidePlace != QLatin1Char('/')) {
        // Avoid "500 GiB Internal Hard Drive/foo/bar" when path originates directly from root.
        const bool isRoot = placeUrl.isLocalFile() && placeUrl.path() == QLatin1Char('/');
        if (isRoot) {
            result = pathInsidePlace;
        } else {
            result.append(pathInsidePlace);
        }
    }

    return result;
}

void KFilePlacesModelPrivate::initDeviceList()
{
    Solid::DeviceNotifier *notifier = Solid::DeviceNotifier::instance();

    QObject::connect(notifier, &Solid::DeviceNotifier::deviceAdded, q, [this](const QString &device) {
        deviceAdded(device);
    });
    QObject::connect(notifier, &Solid::DeviceNotifier::deviceRemoved, q, [this](const QString &device) {
        deviceRemoved(device);
    });

    availableDevices = Solid::Device::listFromQuery(predicate);

    reloadBookmarks();
}

void KFilePlacesModelPrivate::deviceAdded(const QString &udi)
{
    Solid::Device d(udi);

    if (predicate.matches(d)) {
        availableDevices << d;
        reloadBookmarks();
    }
}

void KFilePlacesModelPrivate::deviceRemoved(const QString &udi)
{
    auto it = std::find_if(availableDevices.begin(), availableDevices.end(), [udi](const Solid::Device &device) {
        return device.udi() == udi;
    });
    if (it != availableDevices.end()) {
        availableDevices.erase(it);
        reloadBookmarks();
    }
}

void KFilePlacesModelPrivate::itemChanged(const QString &id, const QList<int> &roles)
{
    for (int row = 0; row < items.size(); ++row) {
        if (items.at(row)->id() == id) {
            QModelIndex index = q->index(row, 0);
            Q_EMIT q->dataChanged(index, index, roles);
        }
    }
}

void KFilePlacesModelPrivate::reloadBookmarks()
{
    QList<KFilePlacesItem *> currentItems = loadBookmarkList();

    QList<KFilePlacesItem *>::Iterator it_i = items.begin();
    QList<KFilePlacesItem *>::Iterator it_c = currentItems.begin();

    QList<KFilePlacesItem *>::Iterator end_i = items.end();
    QList<KFilePlacesItem *>::Iterator end_c = currentItems.end();

    while (it_i != end_i || it_c != end_c) {
        if (it_i == end_i && it_c != end_c) {
            int row = items.count();

            q->beginInsertRows(QModelIndex(), row, row);
            it_i = items.insert(it_i, *it_c);
            ++it_i;
            it_c = currentItems.erase(it_c);

            end_i = items.end();
            end_c = currentItems.end();
            q->endInsertRows();

        } else if (it_i != end_i && it_c == end_c) {
            int row = items.indexOf(*it_i);

            q->beginRemoveRows(QModelIndex(), row, row);
            delete *it_i;
            it_i = items.erase(it_i);

            end_i = items.end();
            end_c = currentItems.end();
            q->endRemoveRows();

        } else if ((*it_i)->id() == (*it_c)->id()) {
            bool shouldEmit = !((*it_i)->bookmark() == (*it_c)->bookmark());
            (*it_i)->setBookmark((*it_c)->bookmark());
            if (shouldEmit) {
                int row = items.indexOf(*it_i);
                QModelIndex idx = q->index(row, 0);
                Q_EMIT q->dataChanged(idx, idx);
            }
            ++it_i;
            ++it_c;
        } else {
            int row = items.indexOf(*it_i);

            if (it_i + 1 != end_i && (*(it_i + 1))->id() == (*it_c)->id()) { // if the next one matches, it's a remove
                q->beginRemoveRows(QModelIndex(), row, row);
                delete *it_i;
                it_i = items.erase(it_i);

                end_i = items.end();
                end_c = currentItems.end();
                q->endRemoveRows();
            } else {
                q->beginInsertRows(QModelIndex(), row, row);
                it_i = items.insert(it_i, *it_c);
                ++it_i;
                it_c = currentItems.erase(it_c);

                end_i = items.end();
                end_c = currentItems.end();
                q->endInsertRows();
            }
        }
    }

    qDeleteAll(currentItems);
    currentItems.clear();

    Q_EMIT q->reloaded();
}

bool KFilePlacesModelPrivate::isBalooUrl(const QUrl &url) const
{
    const QString scheme = url.scheme();
    return ((scheme == QLatin1String("timeline")) || (scheme == QLatin1String("search")));
}

QList<KFilePlacesItem *> KFilePlacesModelPrivate::loadBookmarkList()
{
    QList<KFilePlacesItem *> items;

    KBookmarkGroup root = bookmarkManager->root();
    KBookmark bookmark = root.first();
    QList<Solid::Device> devices{availableDevices};
    QList<QString> tagsList = tags;

    while (!bookmark.isNull()) {
        KFilePlacesItem *item = nullptr;

        if (const QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); !udi.isEmpty()) {
            const QString uuid = bookmark.metaDataItem(QStringLiteral("uuid"));
            auto it = std::find_if(devices.begin(), devices.end(), [udi, uuid](const Solid::Device &device) {
                if (!uuid.isEmpty()) {
                    auto storageVolume = device.as<Solid::StorageVolume>();
                    if (storageVolume && !storageVolume->uuid().isEmpty()) {
                        return storageVolume->uuid() == uuid;
                    }
                }

                return device.udi() == udi;
            });
            if (it != devices.end()) {
                item = new KFilePlacesItem(bookmarkManager, bookmark.address(), it->udi(), q);
                if (!item->hasSupportedScheme(supportedSchemes)) {
                    delete item;
                    item = nullptr;
                }
                devices.erase(it);
            }
        } else if (const QString tag = bookmark.metaDataItem(QStringLiteral("tag")); !tag.isEmpty()) {
            auto it = std::find(tagsList.begin(), tagsList.end(), tag);
            if (it != tagsList.end()) {
                tagsList.erase(it);
                item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q);
            }
        } else if (const QUrl url = bookmark.url(); url.isValid()) {
            QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp"));
            bool allowedHere = appName.isEmpty() || appName == QCoreApplication::instance()->applicationName();
            bool isSupportedUrl = isBalooUrl(url) ? fileIndexingEnabled : true;
            bool isSupportedScheme = supportedSchemes.isEmpty() || supportedSchemes.contains(url.scheme());

            if (isSupportedScheme && isSupportedUrl && allowedHere) {
                // TODO: Update bookmark internal element
                item = new KFilePlacesItem(bookmarkManager, bookmark.address(), QString(), q);
            }
        }

        if (item) {
            QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList<int> &roles) {
                itemChanged(id, roles);
            });
            items << item;
        }

        bookmark = root.next(bookmark);
    }

    // Add bookmarks for the remaining devices, they were previously unknown
    for (const Solid::Device &device : std::as_const(devices)) {
        bookmark = KFilePlacesItem::createDeviceBookmark(bookmarkManager, device);
        if (!bookmark.isNull()) {
            KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), device.udi(), q);
            QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList<int> &roles) {
                itemChanged(id, roles);
            });
            // TODO: Update bookmark internal element
            items << item;
        }
    }

    for (const QString &tag : tagsList) {
        bookmark = KFilePlacesItem::createTagBookmark(bookmarkManager, tag);
        if (!bookmark.isNull()) {
            KFilePlacesItem *item = new KFilePlacesItem(bookmarkManager, bookmark.address(), tag, q);
            QObject::connect(item, &KFilePlacesItem::itemChanged, q, [this](const QString &id, const QList<int> &roles) {
                itemChanged(id, roles);
            });
            items << item;
        }
    }

    // return a sorted list based on groups
    std::stable_sort(items.begin(), items.end(), [](KFilePlacesItem *itemA, KFilePlacesItem *itemB) {
        return (itemA->groupType() < itemB->groupType());
    });

    return items;
}

int KFilePlacesModelPrivate::findNearestPosition(int source, int target)
{
    const KFilePlacesItem *item = items.at(source);
    const KFilePlacesModel::GroupType groupType = item->groupType();
    int newTarget = qMin(target, items.count() - 1);

    // moving inside the same group is ok
    if ((items.at(newTarget)->groupType() == groupType)) {
        return target;
    }

    if (target > source) { // moving down, move it to the end of the group
        int groupFooter = source;
        while (items.at(groupFooter)->groupType() == groupType) {
            groupFooter++;
            // end of the list move it there
            if (groupFooter == items.count()) {
                break;
            }
        }
        target = groupFooter;
    } else { // moving up, move it to beginning of the group
        int groupHead = source;
        while (items.at(groupHead)->groupType() == groupType) {
            groupHead--;
            // beginning of the list move it there
            if (groupHead == 0) {
                break;
            }
        }
        target = groupHead;
    }
    return target;
}

void KFilePlacesModelPrivate::reloadAndSignal()
{
    bookmarkManager->emitChanged(bookmarkManager->root()); // ... we'll get relisted anyway
}

Qt::DropActions KFilePlacesModel::supportedDropActions() const
{
    return Qt::ActionMask;
}

Qt::ItemFlags KFilePlacesModel::flags(const QModelIndex &index) const
{
    Qt::ItemFlags res;

    if (index.isValid()) {
        res |= Qt::ItemIsDragEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
    }

    if (!index.isValid()) {
        res |= Qt::ItemIsDropEnabled;
    }

    return res;
}

QStringList KFilePlacesModel::mimeTypes() const
{
    QStringList types;

    types << KFilePlacesModelPrivate::internalMimeType(this) << QStringLiteral("text/uri-list");

    return types;
}

QMimeData *KFilePlacesModel::mimeData(const QModelIndexList &indexes) const
{
    QList<QUrl> urls;
    QByteArray itemData;

    QDataStream stream(&itemData, QIODevice::WriteOnly);

    for (const QModelIndex &index : std::as_const(indexes)) {
        QUrl itemUrl = url(index);
        if (itemUrl.isValid()) {
            urls << itemUrl;
        }
        stream << index.row();
    }

    QMimeData *mimeData = new QMimeData();

    if (!urls.isEmpty()) {
        mimeData->setUrls(urls);
    }

    mimeData->setData(KFilePlacesModelPrivate::internalMimeType(this), itemData);

    return mimeData;
}

bool KFilePlacesModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    if (action == Qt::IgnoreAction) {
        return true;
    }

    if (column > 0) {
        return false;
    }

    if (row == -1 && parent.isValid()) {
        return false; // Don't allow to move an item onto another one,
        // too easy for the user to mess something up
        // If we really really want to allow copying files this way,
        // let's do it in the views to get the good old drop menu
    }

    if (data->hasFormat(KFilePlacesModelPrivate::ignoreMimeType())) {
        return false;
    }

    if (data->hasFormat(KFilePlacesModelPrivate::internalMimeType(this))) {
        // The operation is an internal move
        QByteArray itemData = data->data(KFilePlacesModelPrivate::internalMimeType(this));
        QDataStream stream(&itemData, QIODevice::ReadOnly);
        int itemRow;

        stream >> itemRow;

        if (!movePlace(itemRow, row)) {
            return false;
        }

    } else if (data->hasFormat(QStringLiteral("text/uri-list"))) {
        // The operation is an add

        QMimeDatabase db;
        KBookmark afterBookmark;

        if (row == -1) {
            // The dropped item is moved or added to the last position

            KFilePlacesItem *lastItem = d->items.last();
            afterBookmark = lastItem->bookmark();

        } else {
            // The dropped item is moved or added before position 'row', ie after position 'row-1'

            if (row > 0) {
                KFilePlacesItem *afterItem = d->items[row - 1];
                afterBookmark = afterItem->bookmark();
            }
        }

        const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(data);

        KBookmarkGroup group = d->bookmarkManager->root();

        for (const QUrl &url : urls) {
            KIO::StatJob *job = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatBasic | KIO::StatResolveSymlink);

            if (!job->exec()) {
                Q_EMIT errorMessage(i18nc("Placeholder is error message", "Could not add to the Places panel: %1", job->errorString()));
                continue;
            }

            KFileItem item(job->statResult(), url, true /*delayed mime types*/);

            if (!item.isDir()) {
                Q_EMIT errorMessage(i18n("Only folders can be added to the Places panel."));
                continue;
            }

            KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, item.text(), url, KIO::iconNameForUrl(url));

            group.moveBookmark(bookmark, afterBookmark);
            afterBookmark = bookmark;
        }

    } else {
        // Oops, shouldn't happen thanks to mimeTypes()
        qWarning() << ": received wrong mimedata, " << data->formats();
        return false;
    }

    refresh();

    return true;
}

void KFilePlacesModel::refresh() const
{
    d->reloadAndSignal();
}

QUrl KFilePlacesModel::convertedUrl(const QUrl &url)
{
    QUrl newUrl = url;
    if (url.scheme() == QLatin1String("timeline")) {
        newUrl = createTimelineUrl(url);
    } else if (url.scheme() == QLatin1String("search")) {
        newUrl = createSearchUrl(url);
    }

    return newUrl;
}

void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName)
{
    addPlace(text, url, iconName, appName, QModelIndex());
}

void KFilePlacesModel::addPlace(const QString &text, const QUrl &url, const QString &iconName, const QString &appName, const QModelIndex &after)
{
    KBookmark bookmark = KFilePlacesItem::createBookmark(d->bookmarkManager, text, url, iconName);

    if (!appName.isEmpty()) {
        bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName);
    }

    if (after.isValid()) {
        KFilePlacesItem *item = static_cast<KFilePlacesItem *>(after.internalPointer());
        d->bookmarkManager->root().moveBookmark(bookmark, item->bookmark());
    }

    refresh();
}

void KFilePlacesModel::editPlace(const QModelIndex &index, const QString &text, const QUrl &url, const QString &iconName, const QString &appName)
{
    if (!index.isValid()) {
        return;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());

    if (item->isDevice()) {
        return;
    }

    KBookmark bookmark = item->bookmark();

    if (bookmark.isNull()) {
        return;
    }

    QList<int> changedRoles;
    bool changed = false;

    if (text != bookmark.fullText()) {
        bookmark.setFullText(text);
        changed = true;
        changedRoles << Qt::DisplayRole;
    }

    if (url != bookmark.url()) {
        bookmark.setUrl(url);
        changed = true;
        changedRoles << KFilePlacesModel::UrlRole;
    }

    if (iconName != bookmark.icon()) {
        bookmark.setIcon(iconName);
        changed = true;
        changedRoles << Qt::DecorationRole;
    }

    const QString onlyInApp = bookmark.metaDataItem(QStringLiteral("OnlyInApp"));
    if (appName != onlyInApp) {
        bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), appName);
        changed = true;
    }

    if (changed) {
        refresh();
        Q_EMIT dataChanged(index, index, changedRoles);
    }
}

void KFilePlacesModel::removePlace(const QModelIndex &index) const
{
    if (!index.isValid()) {
        return;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());

    if (item->isDevice()) {
        return;
    }

    KBookmark bookmark = item->bookmark();

    if (bookmark.isNull()) {
        return;
    }

    d->bookmarkManager->root().deleteBookmark(bookmark);
    refresh();
}

void KFilePlacesModel::setPlaceHidden(const QModelIndex &index, bool hidden)
{
    if (!index.isValid()) {
        return;
    }

    KFilePlacesItem *item = static_cast<KFilePlacesItem *>(index.internalPointer());

    if (item->bookmark().isNull() || item->isHidden() == hidden) {
        return;
    }

    const bool groupHidden = isGroupHidden(item->groupType());
    const bool hidingChildOnShownParent = hidden && !groupHidden;
    const bool showingChildOnShownParent = !hidden && !groupHidden;

    if (hidingChildOnShownParent || showingChildOnShownParent) {
        item->setHidden(hidden);

        d->reloadAndSignal();
        Q_EMIT dataChanged(index, index, {KFilePlacesModel::HiddenRole});
    }
}

void KFilePlacesModel::setGroupHidden(const GroupType type, bool hidden)
{
    if (isGroupHidden(type) == hidden) {
        return;
    }

    d->bookmarkManager->root().setMetaDataItem(stateNameForGroupType(type), (hidden ? QStringLiteral("true") : QStringLiteral("false")));
    d->reloadAndSignal();
    Q_EMIT groupHiddenChanged(type, hidden);
}

bool KFilePlacesModel::movePlace(int itemRow, int row)
{
    KBookmark afterBookmark;

    if ((itemRow < 0) || (itemRow >= d->items.count())) {
        return false;
    }

    if (row >= d->items.count()) {
        row = -1;
    }

    if (row == -1) {
        // The dropped item is moved or added to the last position

        KFilePlacesItem *lastItem = d->items.last();
        afterBookmark = lastItem->bookmark();

    } else {
        // The dropped item is moved or added before position 'row', ie after position 'row-1'

        if (row > 0) {
            KFilePlacesItem *afterItem = d->items[row - 1];
            afterBookmark = afterItem->bookmark();
        }
    }

    KFilePlacesItem *item = d->items[itemRow];
    KBookmark bookmark = item->bookmark();

    int destRow = row == -1 ? d->items.count() : row;

    // avoid move item away from its group
    destRow = d->findNearestPosition(itemRow, destRow);

    // The item is not moved when the drop indicator is on either item edge
    if (itemRow == destRow || itemRow + 1 == destRow) {
        return false;
    }

    beginMoveRows(QModelIndex(), itemRow, itemRow, QModelIndex(), destRow);
    d->bookmarkManager->root().moveBookmark(bookmark, afterBookmark);
    // Move item ourselves so that reloadBookmarks() does not consider
    // the move as a remove + insert.
    //
    // 2nd argument of QList::move() expects the final destination index,
    // but 'row' is the value of the destination index before the moved
    // item has been removed from its original position. That is why we
    // adjust if necessary.
    d->items.move(itemRow, itemRow < destRow ? (destRow - 1) : destRow);
    endMoveRows();

    return true;
}

int KFilePlacesModel::hiddenCount() const
{
    int rows = rowCount();
    int hidden = 0;

    for (int i = 0; i < rows; ++i) {
        if (isHidden(index(i, 0))) {
            hidden++;
        }
    }

    return hidden;
}

QAction *KFilePlacesModel::openRecentOption(const QModelIndex &index) const
{
    const auto bookmark = bookmarkForIndex(index);

    QAction *action = nullptr;

    if (bookmark.url().scheme() == QStringLiteral("recentlyused")) {
        action = new QAction(QIcon::fromTheme(QStringLiteral("settings-configure")), i18nc("@action:inmenu", "Configure…"), nullptr);

        connect(action, &QAction::triggered, this, []() {
            auto job = new KIO::CommandLauncherJob(QStringLiteral("systemsettings"), {QStringLiteral("kcm_recentFiles")});
            job->start();
        });
    }

    return action;
}

QAction *KFilePlacesModel::teardownActionForIndex(const QModelIndex &index) const
{
    Solid::Device device = deviceForIndex(index);

    QAction *action = nullptr;

    if (device.is<Solid::StorageAccess>() && device.as<Solid::StorageAccess>()->isAccessible()) {
        Solid::StorageDrive *drive = device.as<Solid::StorageDrive>();

        if (drive == nullptr) {
            drive = device.parent().as<Solid::StorageDrive>();
        }

        const bool teardownInProgress = deviceAccessibility(index) == KFilePlacesModel::TeardownInProgress;

        bool removable = false;
        if (drive != nullptr) {
            removable = drive->isRemovable();
        }

        QString iconName;
        QString text;

        if (device.is<Solid::OpticalDisc>()) {
            if (teardownInProgress) {
                text = i18nc("@action:inmenu", "Releasing…");
            } else {
                text = i18nc("@action:inmenu", "&Release");
            }
        } else if (removable) {
            if (teardownInProgress) {
                text = i18nc("@action:inmenu", "Safely Removing…");
            } else {
                text = i18nc("@action:inmenu", "&Safely Remove");
            }
            iconName = QStringLiteral("media-eject");
        } else {
            if (teardownInProgress) {
                text = i18nc("@action:inmenu", "Unmounting…");
            } else {
                text = i18nc("@action:inmenu", "&Unmount");
            }
            iconName = QStringLiteral("media-eject");
        }

        if (!iconName.isEmpty()) {
            action = new QAction(QIcon::fromTheme(iconName), text, nullptr);
        } else {
            action = new QAction(text, nullptr);
        }

        if (teardownInProgress) {
            action->setEnabled(false);
        }
    }

    return action;
}

QAction *KFilePlacesModel::ejectActionForIndex(const QModelIndex &index) const
{
    Solid::Device device = deviceForIndex(index);

    if (device.is<Solid::OpticalDisc>()) {
        QString text = i18nc("@action:inmenu", "&Eject");

        return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), text, nullptr);
    }

    return nullptr;
}

void KFilePlacesModel::requestTeardown(const QModelIndex &index)
{
    Solid::Device device = deviceForIndex(index);
    Solid::StorageAccess *access = device.as<Solid::StorageAccess>();

    if (access != nullptr) {
        d->teardownInProgress[access] = index;

        const QString filePath = access->filePath();
        connect(access, &Solid::StorageAccess::teardownDone, this, [this, access, filePath](Solid::ErrorType error, QVariant errorData) {
            d->storageTeardownDone(filePath, error, errorData, access);
        });

        access->teardown();
    }
}

void KFilePlacesModel::requestEject(const QModelIndex &index)
{
    Solid::Device device = deviceForIndex(index);

    Solid::OpticalDrive *drive = device.parent().as<Solid::OpticalDrive>();

    if (drive != nullptr) {
        d->teardownInProgress[drive] = index;

        QString filePath;
        Solid::StorageAccess *access = device.as<Solid::StorageAccess>();
        if (access) {
            filePath = access->filePath();
        }

        connect(drive, &Solid::OpticalDrive::ejectDone, this, [this, filePath, drive](Solid::ErrorType error, QVariant errorData) {
            d->storageTeardownDone(filePath, error, errorData, drive);
        });

        drive->eject();
    } else {
        QString label = data(index, Qt::DisplayRole).toString().replace(QLatin1Char('&'), QLatin1String("&&"));
        QString message = i18n("The device '%1' is not a disk and cannot be ejected.", label);
        Q_EMIT errorMessage(message);
    }
}

void KFilePlacesModel::requestSetup(const QModelIndex &index)
{
    Solid::Device device = deviceForIndex(index);

    if (device.is<Solid::StorageAccess>() && !d->setupInProgress.contains(device.as<Solid::StorageAccess>())
        && !device.as<Solid::StorageAccess>()->isAccessible()) {
        Solid::StorageAccess *access = device.as<Solid::StorageAccess>();

        d->setupInProgress[access] = index;

        connect(access, &Solid::StorageAccess::setupDone, this, [this, access](Solid::ErrorType error, QVariant errorData) {
            d->storageSetupDone(error, errorData, access);
        });

        access->setup();
    }
}

void KFilePlacesModelPrivate::storageSetupDone(Solid::ErrorType error, const QVariant &errorData, Solid::StorageAccess *sender)
{
    QPersistentModelIndex index = setupInProgress.take(sender);

    if (!index.isValid()) {
        return;
    }

    if (!error) {
        Q_EMIT q->setupDone(index, true);
    } else {
        if (errorData.isValid()) {
            Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1', the system responded: %2", q->text(index), errorData.toString()));
        } else {
            Q_EMIT q->errorMessage(i18n("An error occurred while accessing '%1'", q->text(index)));
        }
        Q_EMIT q->setupDone(index, false);
    }
}

void KFilePlacesModelPrivate::storageTeardownDone(const QString &filePath, Solid::ErrorType error, const QVariant &errorData, QObject *sender)
{
    QPersistentModelIndex index = teardownInProgress.take(sender);
    if (!index.isValid()) {
        return;
    }

    if (error == Solid::ErrorType::DeviceBusy && !filePath.isEmpty()) {
        auto *listOpenFilesJob = new KListOpenFilesJob(filePath);
        QObject::connect(listOpenFilesJob, &KIO::Job::result, q, [this, index, error, errorData, listOpenFilesJob]() {
            const auto blockingProcesses = listOpenFilesJob->processInfoList();

            QStringList blockingApps;
            blockingApps.reserve(blockingProcesses.count());
            for (const auto &process : blockingProcesses) {
                blockingApps << process.name();
            }

            Q_EMIT q->teardownDone(index, error, errorData);
            if (blockingProcesses.isEmpty()) {
                Q_EMIT q->errorMessage(i18n("One or more files on this device are open within an application."));
            } else {
                blockingApps.removeDuplicates();
                Q_EMIT q->errorMessage(xi18np("One or more files on this device are opened in application <application>\"%2\"</application>.",
                                              "One or more files on this device are opened in following applications: <application>%2</application>.",
                                              blockingApps.count(),
                                              blockingApps.join(i18nc("separator in list of apps blocking device unmount", ", "))));
            }
        });
        listOpenFilesJob->start();
        return;
    }

    Q_EMIT q->teardownDone(index, error, errorData);
    if (error != Solid::ErrorType::NoError && error != Solid::ErrorType::UserCanceled) {
        Q_EMIT q->errorMessage(errorData.toString());
    }
}

void KFilePlacesModel::setSupportedSchemes(const QStringList &schemes)
{
    d->supportedSchemes = schemes;
    d->reloadBookmarks();
    Q_EMIT supportedSchemesChanged();
}

QStringList KFilePlacesModel::supportedSchemes() const
{
    return d->supportedSchemes;
}

namespace
{
QString partitionManagerPath()
{
    static const QString path = QStandardPaths::findExecutable(QStringLiteral("partitionmanager"));
    return path;
}
} // namespace

QAction *KFilePlacesModel::partitionActionForIndex(const QModelIndex &index) const
{
    const auto device = deviceForIndex(index);
    if (!device.is<Solid::Block>()) {
        return nullptr;
    }

    // Not using kservice to find partitionmanager because we need to manually invoke it so we can pass the --device argument.
    if (partitionManagerPath().isEmpty()) {
        return nullptr;
    }

    auto action =
        new QAction(QIcon::fromTheme(QStringLiteral("partitionmanager")), i18nc("@action:inmenu", "Reformat or Edit with Partition Manager"), nullptr);
    connect(action, &QAction::triggered, this, [device] {
        const auto block = device.as<Solid::Block>();
        auto job = new KIO::CommandLauncherJob(partitionManagerPath(), {QStringLiteral("--device"), block->device()});
        job->setDesktopName(QStringLiteral("org.kde.partitionmanager"));
        job->start();
    });
    return action;
}

#include "moc_kfileplacesmodel.cpp"
