/*
 * SPDX-FileCopyrightText: 2016-2018 Red Hat Inc
 *
 * SPDX-License-Identifier: LGPL-2.0-or-later
 *
 * SPDX-FileCopyrightText: 2016-2018 Jan Grulich <jgrulich@redhat.com>
 * SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
 */

#include "filechooser.h"
#include "filechooser_debug.h"
#include "utils.h"

#include <QDBusArgument>
#include <QDBusMetaType>
#include <QDialogButtonBox>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QGridLayout>
#include <QLabel>
#include <QPushButton>
#include <QQmlApplicationEngine>
#include <QStandardPaths>
#include <QUrl>
#include <QVBoxLayout>
#include <QWindow>

#include <KFileFilterCombo>
#include <KFileWidget>
#include <KLocalizedString>
#include <KSharedConfig>
#include <KWindowConfig>

#include "documents_interface.h"
#include "fuse_interface.h"
#include "request.h"
#include <mobilefiledialog.h>

// Keep in sync with qflatpakfiledialog from flatpak-platform-plugin
Q_DECLARE_METATYPE(FileChooserPortal::Filter)
Q_DECLARE_METATYPE(FileChooserPortal::Filters)
Q_DECLARE_METATYPE(FileChooserPortal::FilterList)
Q_DECLARE_METATYPE(FileChooserPortal::FilterListList)

using namespace Qt::StringLiterals;

QDBusArgument &operator<<(QDBusArgument &arg, const FileChooserPortal::Filter &filter)
{
    arg.beginStructure();
    arg << filter.type << filter.filterString;
    arg.endStructure();
    return arg;
}

const QDBusArgument &operator>>(const QDBusArgument &arg, FileChooserPortal::Filter &filter)
{
    uint type;
    QString filterString;
    arg.beginStructure();
    arg >> type >> filterString;
    filter.type = type;
    filter.filterString = filterString;
    arg.endStructure();

    return arg;
}

QDBusArgument &operator<<(QDBusArgument &arg, const FileChooserPortal::FilterList &filterList)
{
    arg.beginStructure();
    arg << filterList.userVisibleName << filterList.filters;
    arg.endStructure();
    return arg;
}

const QDBusArgument &operator>>(const QDBusArgument &arg, FileChooserPortal::FilterList &filterList)
{
    QString userVisibleName;
    FileChooserPortal::Filters filters;
    arg.beginStructure();
    arg >> userVisibleName >> filters;
    filterList.userVisibleName = userVisibleName;
    filterList.filters = filters;
    arg.endStructure();

    return arg;
}

static bool isKIOFuseAvailable()
{
    static bool available = QDBusConnection::sessionBus().interface()
        && QDBusConnection::sessionBus().interface()->activatableServiceNames().value().contains("org.kde.KIOFuse"_L1);
    return available;
}

// The portal might send us null terminated strings, make sure to strip the extranous \0 in favor of the implicit \0.
// QByteArrays are implicitly terminated already.
static QString decodeFileName(const QByteArray &name)
{
    QByteArray decodedName = name;
    while (decodedName.endsWith('\0')) {
        decodedName.chop(1);
    }
    return QFile::decodeName(decodedName);
}

FileDialog::FileDialog(QDialog *parent, Qt::WindowFlags flags)
    : QDialog(parent, flags)
    , m_fileWidget(new KFileWidget(QUrl(), this))
    , m_configGroup(KSharedConfig::openStateConfig()->group(QStringLiteral("FileDialogSize")))
{
    setLayout(new QVBoxLayout);
    layout()->setContentsMargins({});
    layout()->addWidget(m_fileWidget);

    m_fileWidget->okButton()->show();
    m_fileWidget->cancelButton()->show();

    // accept
    connect(m_fileWidget->okButton(), &QAbstractButton::clicked, m_fileWidget, &KFileWidget::slotOk);
    connect(m_fileWidget, &KFileWidget::accepted, m_fileWidget, &KFileWidget::accept);
    connect(m_fileWidget, &KFileWidget::accepted, this, &QDialog::accept);

    // reject
    connect(m_fileWidget->cancelButton(), &QAbstractButton::clicked, this, &QDialog::reject);
    connect(this, &QDialog::rejected, m_fileWidget, &KFileWidget::slotCancel);

    KSharedConfig::Ptr oldConf = KSharedConfig::openConfig();
    oldConf->group(u"FileDialogSize"_s).moveValuesTo(m_configGroup);

    // restore window size
    if (m_configGroup.exists()) {
        winId(); // ensure there's a window created
        KWindowConfig::restoreWindowSize(windowHandle(), m_configGroup);
        resize(windowHandle()->size());
    }
}

FileDialog::~FileDialog()
{
    // save window size
    KWindowConfig::saveWindowSize(windowHandle(), m_configGroup);
}

FileChooserPortal::FileChooserPortal(QObject *parent)
    : QDBusAbstractAdaptor(parent)
{
    qDBusRegisterMetaType<Filter>();
    qDBusRegisterMetaType<Filters>();
    qDBusRegisterMetaType<FilterList>();
    qDBusRegisterMetaType<FilterListList>();
    qDBusRegisterMetaType<Choice>();
    qDBusRegisterMetaType<Choices>();
    qDBusRegisterMetaType<Option>();
    qDBusRegisterMetaType<OptionList>();
}

static QStringList fuseRedirect(QList<QUrl> urls)
{
    qCDebug(XdgDesktopPortalKdeFileChooser) << "mounting urls with fuse" << urls;

    OrgKdeKIOFuseVFSInterface kiofuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());
    struct MountRequest {
        QDBusPendingReply<QString> reply;
        int urlIndex;
        QString basename;
    };
    QList<MountRequest> requests;
    requests.reserve(urls.count());
    for (int i = 0; i < urls.count(); ++i) {
        QUrl url = urls.at(i);
        if (!url.isLocalFile()) {
            const QString path(url.path());
            const int slashes = path.count(QLatin1Char('/'));
            QString basename;
            if (slashes > 1) {
                url.setPath(path.section(QLatin1Char('/'), 0, slashes - 1));
                basename = path.section(QLatin1Char('/'), slashes, slashes);
            }
            requests.push_back({kiofuse_iface.mountUrl(url.toString()), i, basename});
        }
    }

    for (auto &request : requests) {
        request.reply.waitForFinished();
        if (request.reply.isError()) {
            qWarning() << "FUSE request failed:" << request.reply.error();
            continue;
        }

        urls[request.urlIndex] = QUrl::fromLocalFile(request.reply.value() + QLatin1Char('/') + request.basename);
    };

    qCDebug(XdgDesktopPortalKdeFileChooser) << "mounted urls with fuse, maybe" << urls;

    return QUrl::toStringList(urls, QUrl::FullyEncoded);
}

FileChooserPortal::FilterList FileChooserPortal::fileFilterToFilterList(const KFileFilter &fileFilter)
{
    FilterList filterList;
    filterList.userVisibleName = fileFilter.label();

    for (const QString &mimeFilter : fileFilter.mimePatterns()) {
        Filter filter;
        filter.filterString = mimeFilter;
        filter.type = 1;
        filterList.filters << filter;
    }

    for (const QString &nameFilter : fileFilter.filePatterns()) {
        Filter filter;
        filter.filterString = nameFilter;
        filter.type = 0;
        filterList.filters << filter;
    }

    return filterList;
}

KFileFilter FileChooserPortal::filterListToFileFilter(const FileChooserPortal::FilterList &filterList)
{
    QStringList nameFilters;
    QStringList mimeFilters;

    for (const Filter &filterStruct : filterList.filters) {
        if (filterStruct.type == 0) {
            nameFilters << filterStruct.filterString;
        } else {
            mimeFilters << filterStruct.filterString;
        }
    }

    // Normalize file filters of the form *.[pP][nN][gG] to *.png
    static QRegularExpression filterPattern("\\[([a-z][A-Z]|[A-Z][a-z])\\]"_L1);
    for (auto &filter : nameFilters) {
        if (!filter.startsWith("*."_L1)) {
            continue;
        }
        auto matches = filterPattern.globalMatch(filter, 2);
        QString newFilter = u"*."_s;
        for (const auto &match : matches) {
            const auto first = match.capturedView(1)[0].toLower();
            const auto second = match.capturedView(1)[1].toLower();
            if (first != second) {
                break;
            }
            newFilter += first;
        }
        // 2 chars prefix and then four char become one [pP] -> p
        if ((newFilter.length() - 2) * 4 == filter.length() - 2) {
            filter = newFilter;
        }
    }

    return KFileFilter(filterList.userVisibleName, nameFilters, mimeFilters);
}

enum class Entity {
    File,
    Folder
};
static QUrl kioUrlFromSandboxPath(const QString &path, Entity entity)
{
    if (path.isEmpty()) {
        return {};
    }

    static QString mountPoint;
    if (!mountPoint.isEmpty() && !path.startsWith(mountPoint)) {
        return QUrl::fromLocalFile(path);
    }

    OrgFreedesktopPortalDocumentsInterface documents_iface(QStringLiteral("org.freedesktop.portal.Documents"),
                                                           QStringLiteral("/org/freedesktop/portal/documents"),
                                                           QDBusConnection::sessionBus());

    if (mountPoint.isEmpty()) {
        auto reply = documents_iface.GetMountPoint();
        reply.waitForFinished();
        if (reply.isError()) {
            qCWarning(XdgDesktopPortalKdeFileChooser) << "Failed to get documents mount point:" << reply.error();
        } else {
            mountPoint = QString::fromUtf8(reply);
            if (!path.startsWith(mountPoint)) {
                return QUrl::fromLocalFile(path);
            }
        }
    }

    QByteArray localFilePath;
    switch (entity) {
    case Entity::File: // basename of dirpath (= id of the shared document on the portal)
        localFilePath = documents_iface.Info(QFileInfo(QFileInfo(path).path()).fileName());
        break;
    case Entity::Folder: // basename of path
        localFilePath = documents_iface.Info(QFileInfo(path).fileName());
        break;
    }
    if (localFilePath.isEmpty()) {
        return QUrl::fromLocalFile(path);
    }

    if (!isKIOFuseAvailable()) {
        return QUrl::fromLocalFile(QString::fromLocal8Bit(localFilePath));
    }

    OrgKdeKIOFuseVFSInterface fuse_iface(QStringLiteral("org.kde.KIOFuse"), QStringLiteral("/org/kde/KIOFuse"), QDBusConnection::sessionBus());

    auto reply = fuse_iface.remoteUrl(QString::fromLocal8Bit(localFilePath));
    reply.waitForFinished();
    if (reply.isError()) {
        qCWarning(XdgDesktopPortalKdeFileChooser) << "Failed to get fuse remote URL:" << reply.error();
        return QUrl::fromLocalFile(path);
    }
    QString remoteFilePath = reply.value();
    if (remoteFilePath.isEmpty()) {
        return QUrl::fromLocalFile(path);
    }

    QUrl url(remoteFilePath);
    url.setPath(QFileInfo(url.path()).path());
    qCDebug(XdgDesktopPortalKdeFileChooser) << "Translated portal url to" << url;
    return url;
};

void FileChooserPortal::OpenFile(const QDBusObjectPath &handle,
                                 const QString &app_id,
                                 const QString &parent_window,
                                 const QString &title,
                                 const QVariantMap &options,
                                 const QDBusMessage &message,
                                 uint &replyResponse,
                                 QVariantMap &replyResults)
{
    Q_UNUSED(app_id);

    qCDebug(XdgDesktopPortalKdeFileChooser) << "OpenFile called with parameters:";
    qCDebug(XdgDesktopPortalKdeFileChooser) << "    handle: " << handle.path();
    qCDebug(XdgDesktopPortalKdeFileChooser) << "    parent_window: " << parent_window;
    qCDebug(XdgDesktopPortalKdeFileChooser) << "    title: " << title;
    qCDebug(XdgDesktopPortalKdeFileChooser) << "    options: " << options;

    bool directory = false;
    bool modalDialog = true;
    bool multipleFiles = false;
    QString currentFolder;

    const QString acceptLabel = ExtractAcceptLabel(options);

    if (options.contains(QStringLiteral("modal"))) {
        modalDialog = options.value(QStringLiteral("modal")).toBool();
    }

    if (options.contains(QStringLiteral("multiple"))) {
        multipleFiles = options.value(QStringLiteral("multiple")).toBool();
    }

    if (options.contains(QStringLiteral("directory"))) {
        directory = options.value(QStringLiteral("directory")).toBool();
    }

    if (options.contains(QStringLiteral("current_folder"))) {
        currentFolder = decodeFileName(options.value(QStringLiteral("current_folder")).toByteArray());
        if (QDir::isRelativePath(currentFolder)) {
            qCInfo(XdgDesktopPortalKdeFileChooser) << "ignoring non absolute path" << currentFolder;
            currentFolder.clear();
        }
    }

    const auto [filters, currentFilter] = ExtractFilters(options);

    if (isMobile()) {
        if (m_mobileFileDialog) {
            m_mobileFileDialog->reset(true);
        } else {
            qCDebug(XdgDesktopPortalKdeFileChooser) << "Creating file dialog";
            m_mobileFileDialog = new MobileFileDialog(this);
        }

        m_mobileFileDialog->setTitle(title);

        // Always true when we are opening a file
        m_mobileFileDialog->setSelectExisting(true);

        m_mobileFileDialog->setSelectFolder(directory);

        m_mobileFileDialog->setSelectMultiple(multipleFiles);

        // currentName: not implemented

        if (!currentFolder.isEmpty()) {
            m_mobileFileDialog->setFolder(QUrl::fromLocalFile(currentFolder));
        }

        if (!acceptLabel.isEmpty()) {
            m_mobileFileDialog->setAcceptLabel(acceptLabel);
        }

        if (!filters.isEmpty() && !filters.first().filePatterns().isEmpty()) {
            m_mobileFileDialog->setNameFilters(filters.first().filePatterns());
        }

        if (!filters.isEmpty() && !filters.first().mimePatterns().isEmpty()) {
            m_mobileFileDialog->setMimeTypeFilters(filters.first().mimePatterns());
        }

        replyResponse = m_mobileFileDialog->exec();

        replyResults.insert(QStringLiteral("uris"), fuseRedirect(m_mobileFileDialog->results()));

        return;
    }

    const QUrl translatedCurrentFolderUrl = kioUrlFromSandboxPath(currentFolder, Entity::Folder);

    // Use QFileDialog for most directory requests to utilize
    // plasma-integration's KDirSelectDialog
    if (directory && !options.contains(QStringLiteral("choices"))) {
        auto dirDialog = new QFileDialog;
        dirDialog->setWindowTitle(title);
        dirDialog->setFileMode(QFileDialog::Directory);
        dirDialog->setOptions(QFileDialog::ShowDirsOnly);
        if (!isKIOFuseAvailable()) {
            dirDialog->setSupportedSchemes(QStringList{QStringLiteral("file")});
        }
        if (!acceptLabel.isEmpty()) {
            dirDialog->setLabelText(QFileDialog::Accept, acceptLabel);
        }
        if (!translatedCurrentFolderUrl.isEmpty()) {
            dirDialog->setDirectoryUrl(translatedCurrentFolderUrl);
        }

        dirDialog->winId(); // Trigger window creation
        Utils::setParentWindow(dirDialog, parent_window);
        Request::makeClosableDialogRequest(handle, dirDialog);
        dirDialog->setWindowModality(modalDialog ? Qt::WindowModal : Qt::NonModal);

        delayReply(message, dirDialog, this, [dirDialog](int result) -> QVariantList {
            dirDialog->deleteLater();
            if (result != QDialog::Accepted) {
                return {PortalResponse::Cancelled, QVariantMap{}};
            }
            const auto urls = dirDialog->selectedUrls();
            if (urls.empty()) {
                return {PortalResponse::OtherError, QVariantMap{}};
            }
            return {PortalResponse::Success, QVariantMap{{QStringLiteral("uris"), fuseRedirect(urls)}, {QStringLiteral("writable"), true}}};
        });
        dirDialog->open();
        return;
    }

    // for handling of options - choices
    QWidget *optionsWidget = nullptr;
    // to store IDs for choices along with corresponding comboboxes/checkboxes
    QMap<QString, QCheckBox *> checkboxes;
    QMap<QString, QComboBox *> comboboxes;

    auto fileDialog = new FileDialog();
    Utils::setParentWindow(fileDialog, parent_window);
    Request::makeClosableDialogRequest(handle, fileDialog);
    fileDialog->setWindowModality(modalDialog ? Qt::WindowModal : Qt::NonModal);

    if (options.contains(QStringLiteral("choices"))) {
        OptionList optionList = qdbus_cast<OptionList>(options.value(QStringLiteral("choices")));
        optionsWidget = CreateChoiceControls(optionList, checkboxes, comboboxes);
    }

    fileDialog->setWindowTitle(title);
    KFile::Mode mode = directory ? KFile::Mode::Directory : multipleFiles ? KFile::Mode::Files : KFile::Mode::File;
    fileDialog->m_fileWidget->setMode(mode | KFile::Mode::ExistingOnly);
    if (!isKIOFuseAvailable()) {
        fileDialog->m_fileWidget->setSupportedSchemes(QStringList{QStringLiteral("file")});
    }
    fileDialog->m_fileWidget->okButton()->setText(!acceptLabel.isEmpty() ? acceptLabel : i18n("Open"));

    if (!translatedCurrentFolderUrl.isEmpty()) {
        fileDialog->m_fileWidget->setUrl(translatedCurrentFolderUrl);
    }

    fileDialog->m_fileWidget->setFilters(filters, currentFilter);

    if (optionsWidget) {
        fileDialog->m_fileWidget->setCustomWidget({}, optionsWidget);
    }

    delayReply(message, fileDialog, this, [fileDialog, optionsWidget, checkboxes, comboboxes](int dialogResult) -> QVariantList {
        fileDialog->deleteLater();
        QVariantMap results;
        if (dialogResult == QDialog::Accepted) {
            const auto urls = fileDialog->m_fileWidget->selectedUrls();
            if (urls.isEmpty()) {
                return {PortalResponse::Cancelled, results};
            }
            results.insert(QStringLiteral("uris"), fuseRedirect(urls));
            results.insert(QStringLiteral("writable"), true);
            if (optionsWidget) {
                QVariant choices = EvaluateSelectedChoices(checkboxes, comboboxes);
                results.insert(QStringLiteral("choices"), choices);
            }
            FilterList currentFilter = fileFilterToFilterList(fileDialog->m_fileWidget->currentFilter());
            results.insert(QStringLiteral("current_filter"), QVariant::fromValue<FilterList>(currentFilter));
        }
        return {PortalResponse::fromDialogCode(static_cast<QDialog::DialogCode>(dialogResult)), results};
    });
    fileDialog->open();
}

void FileChooserPortal::SaveFile(const QDBusObjectPath &handle,
                                 const QString &app_id,
                                 const QString &parent_window,
                                 const QString &title,
                                 const QVariantMap &options,
                                 const QDBusMessage &message,
                                 uint &replyResponse,
                                 QVariantMap &replyResults)
{
    Q_UNUSED(app_id);

    qCDebug(XdgDesktopPortalKdeFileChooser) << "SaveFile called with parameters:";
    qCDebug(XdgDesktopPortalKdeFileChooser) << "    handle: " << handle.path();
    qCDebug(XdgDesktopPortalKdeFileChooser) << "    parent_window: " << parent_window;
    qCDebug(XdgDesktopPortalKdeFileChooser) << "    title: " << title;
    qCDebug(XdgDesktopPortalKdeFileChooser) << "    options: " << options;

    bool modalDialog = true;
    QString currentName;
    QString currentFolder;
    QString currentFile;

    if (options.contains(QStringLiteral("modal"))) {
        modalDialog = options.value(QStringLiteral("modal")).toBool();
    }

    const QString acceptLabel = ExtractAcceptLabel(options);

    if (options.contains(QStringLiteral("current_name"))) {
        currentName = options.value(QStringLiteral("current_name")).toString();
    }

    if (options.contains(QStringLiteral("current_folder"))) {
        currentFolder = decodeFileName(options.value(QStringLiteral("current_folder")).toByteArray());
        if (QDir::isRelativePath(currentFolder)) {
            qCInfo(XdgDesktopPortalKdeFileChooser) << "ignoring non absolute path" << currentFolder;
            currentFolder.clear();
        }
    }

    if (options.contains(QStringLiteral("current_file"))) {
        currentFile = decodeFileName(options.value(QStringLiteral("current_file")).toByteArray());
    }

    const auto [filters, currentFilter] = ExtractFilters(options);

    if (isMobile()) {
        if (m_mobileFileDialog) {
            m_mobileFileDialog->reset(true);
        } else {
            qCDebug(XdgDesktopPortalKdeFileChooser) << "Creating file dialog";
            m_mobileFileDialog = new MobileFileDialog(this);
        }

        m_mobileFileDialog->setTitle(title);

        // Always false when we are saving a file
        m_mobileFileDialog->setSelectExisting(false);

        if (!currentFolder.isEmpty()) {
            // Set correct protocol
            m_mobileFileDialog->setFolder(QUrl::fromLocalFile(currentFolder));
        }

        if (!currentFile.isEmpty()) {
            m_mobileFileDialog->setCurrentFile(currentFile);
        }

        // currentName: not implemented

        if (!acceptLabel.isEmpty()) {
            m_mobileFileDialog->setAcceptLabel(acceptLabel);
        }

        if (!filters.isEmpty() && !filters.first().filePatterns().isEmpty()) {
            m_mobileFileDialog->setNameFilters(filters.first().filePatterns());
        }

        if (!filters.isEmpty() && !filters.first().mimePatterns().isEmpty()) {
            m_mobileFileDialog->setMimeTypeFilters(filters.first().mimePatterns());
        }

        replyResponse = m_mobileFileDialog->exec();

        replyResults.insert(QStringLiteral("uris"), fuseRedirect(m_mobileFileDialog->results()));

        return;
    }

    // for handling of options - choices
    QWidget *optionsWidget = nullptr;
    // to store IDs for choices along with corresponding comboboxes/checkboxes
    QMap<QString, QCheckBox *> checkboxes;
    QMap<QString, QComboBox *> comboboxes;

    auto fileDialog = new FileDialog();
    Utils::setParentWindow(fileDialog, parent_window);
    Request::makeClosableDialogRequest(handle, fileDialog);
    fileDialog->setWindowModality(modalDialog ? Qt::WindowModal : Qt::NonModal);

    if (options.contains(QStringLiteral("choices"))) {
        OptionList optionList = qdbus_cast<OptionList>(options.value(QStringLiteral("choices")));
        optionsWidget = CreateChoiceControls(optionList, checkboxes, comboboxes);
    }

    fileDialog->setWindowTitle(title);
    fileDialog->m_fileWidget->setOperationMode(KFileWidget::Saving);
    fileDialog->m_fileWidget->setConfirmOverwrite(true);

    const QUrl translatedCurrentFolderUrl = kioUrlFromSandboxPath(currentFolder, Entity::Folder);
    if (!translatedCurrentFolderUrl.isEmpty()) {
        fileDialog->m_fileWidget->setUrl(translatedCurrentFolderUrl);
    }

    if (!currentFile.isEmpty()) {
        // If we also had a currentfolder then recycle its URL, otherwise calculate from scratch.
        // In either case append the basename to get to a complete file URL again.
        QUrl kioUrl = translatedCurrentFolderUrl.isEmpty() ? kioUrlFromSandboxPath(currentFile, Entity::File) : translatedCurrentFolderUrl;
        if (!kioUrl.isEmpty()) {
            kioUrl.setPath(kioUrl.path() + QLatin1Char('/') + QFileInfo(currentFile).completeBaseName());
            fileDialog->m_fileWidget->setSelectedUrl(kioUrl);
        } else {
            fileDialog->m_fileWidget->setSelectedUrl(QUrl::fromLocalFile(currentFile));
        }
    }

    if (!currentName.isEmpty()) {
        QUrl url = fileDialog->m_fileWidget->baseUrl();
        QString path = url.path();
        if (path.back() == QLatin1Char('/')) {
            path = path + currentName;
        } else {
            path = path + QLatin1Char('/') + currentName;
        }
        url.setPath(path);
        fileDialog->m_fileWidget->setSelectedUrl(url);
    }

    if (!acceptLabel.isEmpty()) {
        fileDialog->m_fileWidget->okButton()->setText(acceptLabel);
    }

    fileDialog->m_fileWidget->setFilters(filters, currentFilter);

    if (optionsWidget) {
        fileDialog->m_fileWidget->setCustomWidget(optionsWidget);
    }

    delayReply(message, fileDialog, this, [fileDialog, optionsWidget, checkboxes, comboboxes](int dialogResult) {
        QVariantMap results;
        fileDialog->deleteLater();
        if (dialogResult == QDialog::Accepted) {
            const auto urls = fileDialog->m_fileWidget->selectedUrls();
            results.insert(QStringLiteral("uris"), fuseRedirect(urls));
            if (optionsWidget) {
                QVariant choices = EvaluateSelectedChoices(checkboxes, comboboxes);
                results.insert(QStringLiteral("choices"), choices);
            }
            FilterList currentFilter = fileFilterToFilterList(fileDialog->m_fileWidget->currentFilter());
            results.insert(QStringLiteral("current_filter"), QVariant::fromValue<FilterList>(currentFilter));
        }
        return QVariantList{PortalResponse::fromDialogCode(static_cast<QDialog::DialogCode>(dialogResult)), results};
    });
    fileDialog->open();
}

QWidget *FileChooserPortal::CreateChoiceControls(const OptionList &optionList, QMap<QString, QCheckBox *> &checkboxes, QMap<QString, QComboBox *> &comboboxes)
{
    if (optionList.empty()) {
        return nullptr;
    }

    QWidget *optionsWidget = new QWidget;
    QGridLayout *layout = new QGridLayout(optionsWidget);
    // set stretch for (unused) column 2 so controls only take the space they actually need
    layout->setColumnStretch(2, 1);
    optionsWidget->setLayout(layout);

    for (const Option &option : optionList) {
        const int nextRow = layout->rowCount();
        // empty list of choices -> boolean choice according to the spec
        if (option.choices.empty()) {
            QCheckBox *checkbox = new QCheckBox(option.label, optionsWidget);
            checkbox->setChecked(option.initialChoiceId == QStringLiteral("true"));
            layout->addWidget(checkbox, nextRow, 1);
            checkboxes.insert(option.id, checkbox);
        } else {
            QComboBox *combobox = new QComboBox(optionsWidget);
            for (const Choice &choice : option.choices) {
                combobox->addItem(choice.value, choice.id);
                // select this entry if initialChoiceId matches
                if (choice.id == option.initialChoiceId) {
                    combobox->setCurrentIndex(combobox->count() - 1);
                }
            }
            QString labelText = option.label;
            if (!labelText.endsWith(QChar::fromLatin1(':'))) {
                labelText += QChar::fromLatin1(':');
            }
            QLabel *label = new QLabel(labelText, optionsWidget);
            label->setBuddy(combobox);
            layout->addWidget(label, nextRow, 0, Qt::AlignRight);
            layout->addWidget(combobox, nextRow, 1);
            comboboxes.insert(option.id, combobox);
        }
    }

    return optionsWidget;
}

QVariant FileChooserPortal::EvaluateSelectedChoices(const QMap<QString, QCheckBox *> &checkboxes, const QMap<QString, QComboBox *> &comboboxes)
{
    Choices selectedChoices;
    const auto checkboxKeys = checkboxes.keys();
    for (const QString &id : checkboxKeys) {
        Choice choice;
        choice.id = id;
        choice.value = checkboxes.value(id)->isChecked() ? QStringLiteral("true") : QStringLiteral("false");
        selectedChoices << choice;
    }
    const auto comboboxKeys = comboboxes.keys();
    for (const QString &id : comboboxKeys) {
        Choice choice;
        choice.id = id;
        choice.value = comboboxes.value(id)->currentData().toString();
        selectedChoices << choice;
    }

    return QVariant::fromValue<Choices>(selectedChoices);
}

QString FileChooserPortal::ExtractAcceptLabel(const QVariantMap &options)
{
    QString acceptLabel;
    if (options.contains(QStringLiteral("accept_label"))) {
        acceptLabel = options.value(QStringLiteral("accept_label")).toString();
        // 'accept_label' allows mnemonic underlines, but Qt uses '&' character, so replace/escape accordingly
        // to keep literal '&'s and transform mnemonic underlines to the Qt equivalent using '&' for mnemonic
        acceptLabel.replace(QChar::fromLatin1('&'), QStringLiteral("&&"));
        const int mnemonic_pos = acceptLabel.indexOf(QChar::fromLatin1('_'));
        if (mnemonic_pos != -1) {
            acceptLabel.replace(mnemonic_pos, 1, QChar::fromLatin1('&'));
        }
    }
    return acceptLabel;
}

FileChooserPortal::FilterExtract FileChooserPortal::ExtractFilters(const QVariantMap &options)
{
    FilterExtract result;

    if (options.contains(QStringLiteral("filters"))) {
        const FilterListList filterListList = qdbus_cast<FilterListList>(options.value(QStringLiteral("filters")));

        for (const FilterList &filterList : filterListList) {
            result.filters << filterListToFileFilter(filterList);
        }
    }

    if (options.contains(QStringLiteral("current_filter"))) {
        FilterList filterList = qdbus_cast<FilterList>(options.value(QStringLiteral("current_filter")));
        if (filterList.filters.size() >= 1) {
            result.currentFilter = filterListToFileFilter(filterList);
        } else {
            qCDebug(XdgDesktopPortalKdeFileChooser) << "Ignoring 'current_filter' parameter with no filters specified.";
        }
    }

    return result;
}

bool FileChooserPortal::isMobile()
{
    QByteArray mobile = qgetenv("QT_QUICK_CONTROLS_MOBILE");
    return mobile == "true" || mobile == "1";
}

#include "moc_filechooser.cpp"
