/***********************************************************************
 *                                                                     *
 * Copyright 2016  Lorenzo Porta (Vindex17) <vindex17@outlook.it>      *
 *                                                                     *
 * This program is free software; you can redistribute it and/or       *
 * modify it under the terms of the GNU General Public License as      *
 * published by the Free Software Foundation; either version 3 of      *
 * the License or any later version accepted by the membership of      *
 * KDE e.V. (or its successor approved by the membership of KDE        *
 * e.V.), which shall act as a proxy defined in Section 14 of          *
 * version 3 of the license.                                           *
 *                                                                     *
 * This program is distributed in the hope that it will be useful,     *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of      *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the       *
 * GNU General Public License for more details.                        *
 *                                                                     *
 * You should have received a copy of the GNU General Public License   *
 * along with this program. If not, see <http://www.gnu.org/licenses/> *
 *                                                                     *
 ***********************************************************************/


#include "kfoldersync.hxx"
#include "kfoldersync.moc"
#include "previewdialog.hxx"
#include "profiledialog.hxx"
#include "progressdialogs.hxx"

#include <KAboutData>
#include <KDBusService>
#include <KHelpMenu>
#include <KIO/CopyJob>
#include <KIO/DeleteJob>
#include <KIO/JobUiDelegate>
#include <KStandardAction>
#include <KStandardGuiItem>
#include <KStartupInfo>
#include <KWindowSystem>
#include <QDir>
#include <QGridLayout>
#include <QGroupBox>
#include <QFileDialog>
#include <QInputDialog>
#include <QMenu>
#include <QPushButton>
#include <QStandardPaths>
#include <QToolBar>
#include <QtConcurrentMap>
#include <mutex>


#define COPY_FILE(SIDE1, SIDE2) \
    const QUrl &url1 = init_child_url(SIDE1##_url, file_map_entry->first); \
    const QUrl &url2 = init_child_url(SIDE2##_url, file_map_entry->first); \
    result.task_deque.emplace_back(std::bind(KIO::copyAs, url1, url2, default_kiojob_flags), copy_description_template.arg(url1.toDisplayString(URL_DISPLAY_FORMAT), url2.toDisplayString(URL_DISPLAY_FORMAT))); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_copy_in_##SIDE2, std::placeholders::_1, icon_for(url1), file_map_entry->first, file_map_entry->second.size_in_##SIDE1))

#define LOOP_FILE_MAP  for (auto file_map_entry = file_map.cbegin(); Q_LIKELY(file_map_entry != file_map_end); file_map_entry = file_map.erase(file_map_entry))
// ↓
#define SUBLOOP_FILE_MAP \
    for (auto next_file_map_entry = std::next(file_map_entry); Q_LIKELY(next_file_map_entry != file_map_end); next_file_map_entry = file_map.erase(next_file_map_entry))
// ↓
#define UPDATE_PROGRESS_DIALOG progress_dialog.update(); event_loop.processEvents();  if (Q_UNLIKELY(progress_dialog.isHidden())) return Phase1Result()
// ↓
#define DISCARD_DIR_CONTENTS SUBLOOP_FILE_MAP if (Q_LIKELY(next_file_map_entry->first.startsWith(file_map_entry->first))) {UPDATE_PROGRESS_DIALOG;} else break

#define MKDIR(SIDE) \
    const QUrl &url = init_child_url(SIDE##_url, file_map_entry->first); \
    result.task_deque.emplace_back(std::bind(KIO::mkdir, url, -1), mkdir_description_template.arg(url.toDisplayString(URL_DISPLAY_FORMAT))); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_mkdir_in_##SIDE, std::placeholders::_1, file_map_entry->first))

#define OVERWRITE_FILE(SIDE1, SIDE2) \
    const QUrl &url1 = init_child_url(SIDE1##_url, file_map_entry->first); \
    const QUrl &url2 = init_child_url(SIDE2##_url, file_map_entry->first); \
    result.task_deque.emplace_back(std::bind(KIO::copyAs, url1, url2, default_kiojob_flags | KIO::Overwrite), overwrite_description_template.arg(url1.toDisplayString(URL_DISPLAY_FORMAT), url2.toDisplayString(URL_DISPLAY_FORMAT))); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_overwrite_in_##SIDE2, std::placeholders::_1, icon_for(url1), icon_for(url2), file_map_entry->first,  file_map_entry->second.size_in_##SIDE1, file_map_entry->second.size_in_##SIDE2))

#define RENAME_AND_COPY_FILE(SIDE1, SIDE2) \
    const QUrl &url1 = init_child_url(SIDE1##_url, file_map_entry->first); \
    const QUrl &url2 = init_child_url(SIDE2##_url, file_map_entry->first); \
    const QUrl url3(url2) ; \
    const_cast<QUrl&>(url3).setPath(url3.path() % QStringLiteral(" - ") % QDateTime::currentDateTime().toString(Qt::SystemLocaleLongDate)); \
    const QString &displayable_url2 = url2.toDisplayString(URL_DISPLAY_FORMAT); \
    result.task_deque.emplace_back(std::bind(KIO::rename, url2, url3, default_kiojob_flags), rename_description_template.arg(displayable_url2, url3.toDisplayString(URL_DISPLAY_FORMAT))); \
    result.task_deque.emplace_back(std::bind(KIO::copyAs, url1, url2, default_kiojob_flags), copy_description_template.arg(url1.toDisplayString(URL_DISPLAY_FORMAT), displayable_url2)); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_rename_in_##SIDE2, std::placeholders::_1, icon_for(url2), file_map_entry->first)); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_copy_in_##SIDE2, std::placeholders::_1, icon_for(url1), file_map_entry->first, file_map_entry->second.size_in_##SIDE1))

#define RENAME_AND_MKDIR(SIDE) \
    const QUrl &url1 = init_child_url(SIDE##_url, file_map_entry->first); \
    const QUrl url2(url1); \
    const_cast<QUrl&>(url2).setPath(url2.path() % QStringLiteral(" - ") % QDateTime::currentDateTime().toString(Qt::SystemLocaleLongDate)); \
    const QString &displayable_url1 = url1.toDisplayString(URL_DISPLAY_FORMAT); \
    result.task_deque.emplace_back(std::bind(KIO::rename, url1, url2, default_kiojob_flags), rename_description_template.arg(displayable_url1, url2.toDisplayString(URL_DISPLAY_FORMAT))); \
    result.task_deque.emplace_back(std::bind(KIO::mkdir, url1, -1), mkdir_description_template.arg(displayable_url1)); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_rename_in_##SIDE, std::placeholders::_1, icon_for(url1), file_map_entry->first)); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_mkdir_in_##SIDE, std::placeholders::_1, file_map_entry->first))

#define REPLACE_DIR_WITH_FILE(SIDE1, SIDE2) \
    const QUrl &url1 = init_child_url(SIDE1##_url, file_map_entry->first); \
    const QUrl &url2 = init_child_url(SIDE2##_url, file_map_entry->first); \
    const QString &displayable_url2 = url2.toDisplayString(URL_DISPLAY_FORMAT); \
    if (use_trash)  result.task_deque.emplace_back(std::bind<KIO::CopyJob*(const QUrl&, KIO::JobFlags)>(KIO::trash, url2, default_kiojob_flags), trash_description_template.arg(displayable_url2)); \
    else  result.task_deque.emplace_back(std::bind<KIO::DeleteJob*(const QUrl&, KIO::JobFlags)>(KIO::del, url2, default_kiojob_flags), del_description_template.arg(displayable_url2)); \
    KIO::filesize_t size2 = file_map_entry->second.size_in_##SIDE2; \
    SUBLOOP_FILE_MAP {if (Q_LIKELY(next_file_map_entry->first.startsWith(file_map_entry->first))) size2 += next_file_map_entry->second.size_in_##SIDE2; else break;} \
    result.task_deque.emplace_back(std::bind(KIO::copyAs, url1, url2, default_kiojob_flags), copy_description_template.arg(url1.toDisplayString(URL_DISPLAY_FORMAT), displayable_url2)); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_del_in_##SIDE2, std::placeholders::_1, icon_for(url2), file_map_entry->first, size2)); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_copy_in_##SIDE2, std::placeholders::_1, icon_for(url1), file_map_entry->first, file_map_entry->second.size_in_##SIDE1))

#define REPLACE_FILE_WITH_DIR(SIDE) \
    const QUrl &url = init_child_url(SIDE##_url, file_map_entry->first); \
    const QString &displayable_url = url.toDisplayString(URL_DISPLAY_FORMAT); \
    if (use_trash) result.task_deque.emplace_back(std::bind<KIO::CopyJob*(const QUrl&, KIO::JobFlags)>(KIO::trash, url, default_kiojob_flags), trash_description_template.arg(displayable_url)); \
    else result.task_deque.emplace_back(std::bind(KIO::file_delete, url, default_kiojob_flags), del_description_template.arg(displayable_url)); \
    result.task_deque.emplace_back(std::bind(KIO::mkdir, url, -1), mkdir_description_template.arg(displayable_url)); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_del_in_##SIDE, std::placeholders::_1, icon_for(url), file_map_entry->first, file_map_entry->second.size_in_##SIDE)); \
    preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_mkdir_in_##SIDE, std::placeholders::_1, file_map_entry->first))

#define SETUP_CHECKBOX(PREFIX, ICON, TOOLTIP) \
    PREFIX##_checkbox.setIcon(ICON); \
    PREFIX##_checkbox.setToolTip(TOOLTIP); \
    option_groupbox_layout->addWidget(&PREFIX##_checkbox)

#define SETUP_LISTJOB(THIS, THE_OTHER, TIME_OP, MATCH_FILE, MATCH_DIR) \
    Q_ASSERT(THIS##_listjob.isNull()); \
    THIS##_listjob = KIO::listRecursive(THIS##_url, KIO::HideProgressInfo, include_hidden_files_checkbox.isChecked()); \
    Q_ASSERT(THIS##_listjob->isAutoDelete()); \
    Q_ASSERT(THIS##_listjob->uiDelegate()->isAutoErrorHandlingEnabled() == false); \
    Q_ASSERT(THIS##_listjob->uiDelegate()->isAutoWarningHandlingEnabled()); \
    THIS##_listjob->uiDelegate()->setAutoErrorHandlingEnabled(true); \
    if (logger) {connect(THIS##_listjob, &KIO::ListJob::infoMessage, logger, &Logger::log_kiojob_info, Qt::DirectConnection); connect(THIS##_listjob, &KIO::ListJob::warning, logger, &Logger::log_kiojob_info, Qt::DirectConnection);} \
    connect(THIS##_listjob, &KIO::ListJob::entries, [&](void *, const KIO::UDSEntryList &udsentry_list){ \
        QtConcurrent::blockingMap(udsentry_list, [&](const KIO::UDSEntry &udsentry){ \
            QString current_path = udsentry.stringValue(KIO::UDSEntry::UDS_NAME); \
            for (const QString &path_to_exclude : path_to_exclude_vector) if (current_path.startsWith(path_to_exclude)) return; \
            for (const QRegularExpression &pcre : pcre_list) if (pcre.match(current_path).hasMatch()) return; \
            current_path.squeeze(); \
            mutex.lock(); \
            FileMapEntryInfo &entry_info = file_map[std::move(current_path)]; \
            mutex.unlock(); \
            entry_info.size_in_##THIS = KIO::filesize_t(udsentry.numberValue(KIO::UDSEntry::UDS_SIZE)); \
            entry_info.relative_time TIME_OP udsentry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME); \
            entry_info.match += Q_LIKELY(udsentry.isDir() == false || Q_UNLIKELY(udsentry.isLink())) ? FileMapEntryInfo::MATCH_FILE : FileMapEntryInfo::MATCH_DIR; \
        }); \
    }); \
    connect(THIS##_listjob, &KIO::ListJob::result,  [&]{ \
        const auto &error_code = THIS##_listjob->error(); \
        if (Q_LIKELY(error_code == KIO::ListJob::NoError)) { \
            if (THE_OTHER##_listjob) THIS##_listjob.clear(); \
            else event_loop.exit(error_code); \
        } else { \
            event_loop.exit(error_code); \
            if (logger) {const QString &error_string = THIS##_listjob->errorString(); logger->write(error_string.isEmpty() ? THIS##_listjob->detailedErrorStrings().first() : error_string);} \
            if (THE_OTHER##_listjob) {disconnect(THE_OTHER##_listjob, &KIO::ListJob::entries, nullptr, nullptr); THE_OTHER##_listjob->kill(KIO::ListJob::Quietly);} \
        } \
    }); \
    connect(&progress_dialog, &ProgressDialog::rejected, THIS##_listjob, [&]{THIS##_listjob->kill(KIO::ListJob::EmitResult); progress_dialog.disconnect(THE_OTHER##_listjob);}, Qt::DirectConnection)


__attribute__((always_inline)) static inline QUrl init_child_url(const QUrl &base_url, const QString &relative_path)
{
    QUrl url(base_url);
    Q_ASSERT_X(base_url.path().endsWith(QLatin1Char('/')), qPrintable(base_url.toString()), "URL doesn't end with '/'");
    url.setPath(url.path() % relative_path);
    Q_ASSERT_X(url.isValid(), qPrintable(url.toString()), "Invalid URL");
    return url;
}


KFolderSync::KFolderSync(QApplication * const app_ptr)
{
    // Connect to dbus (keep it without parent to avoid potential SIGABRT)
    static const KDBusService dbus_service(KDBusService::Unique);
    connect(&dbus_service, &KDBusService::activateRequested, this, [&]{KStartupInfo::setNewStartupId(this, KStartupInfo::startupId()); KWindowSystem::forceActiveWindow(winId());}, Qt::DirectConnection);

    // Setup toolbar
    QMenu * const app_menu = new QMenu(this);
    KHelpMenu * const help_menu = new KHelpMenu(this, KAboutData::applicationData(), false);
    app_menu->addAction(KStandardAction::create(KStandardAction::SwitchApplicationLanguage, help_menu, &KHelpMenu::switchApplicationLanguage, nullptr));
    app_menu->addSeparator();
    app_menu->addAction(QIcon(QStringLiteral(":/qt-project.org/qmessagebox/images/qtlogo-64.png")), i18nc("@item:inmenu", "About Qt"), app_ptr, &QApplication::aboutQt);
    app_menu->addAction(KStandardAction::create(KStandardAction::AboutKDE, help_menu, &KHelpMenu::aboutKDE, nullptr));
    app_menu->addAction(KStandardAction::create(KStandardAction::AboutApp, help_menu, &KHelpMenu::aboutApplication, nullptr));
    app_menu->addSeparator();
    app_menu->addAction(KStandardAction::create(KStandardAction::Quit, app_ptr, &QApplication::closeAllWindows, nullptr));
    QToolButton * const app_menu_toolbutton = new QToolButton;
    app_menu_toolbutton->setIcon(QIcon::fromTheme(QStringLiteral("application-menu")));
    app_menu_toolbutton->setMenu(app_menu);
    app_menu_toolbutton->setPopupMode(QToolButton::InstantPopup);
    QToolBar * const main_toolbar = addToolBar(i18nc("@title", "Main toolbar"));
    main_toolbar->setMovable(false);
    main_toolbar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
    main_toolbar->toggleViewAction()->setEnabled(false);
    main_toolbar->addWidget(app_menu_toolbutton);
    main_toolbar->addSeparator();
    main_toolbar->addAction(QIcon::fromTheme(QStringLiteral("kt-queue-manager")), i18nc("@action:intoolbar", "Profiles"), this, &KFolderSync::manage_profiles);
    const KGuiItem &reset_ksgi = KStandardGuiItem::reset();
    main_toolbar->addAction(reset_ksgi.icon(), reset_ksgi.text(), [&]{close(); new KFolderSync(qApp);});
    main_toolbar->addSeparator();
    main_toolbar->addAction(KStandardGuiItem::add().icon(), i18nc("@action:intoolbar", "Add operation"), &url_requester, &UrlRequester::add);
    main_toolbar->addAction(KStandardGuiItem::remove().icon(), i18nc("@action:intoolbar", "Remove operation"), &url_requester, &UrlRequester::remove);
    main_toolbar->addAction(QIcon::fromTheme(QStringLiteral("system-run")), i18nc("@action:intoolbar", "Run"), this, &KFolderSync::run);

    // Setup central widget
    QGroupBox * const operation_groupbox = new QGroupBox(i18nc("@title:group", "Operation"));
    QVBoxLayout * const operation_groupbox_layout = new QVBoxLayout(operation_groupbox);
    operation_groupbox_layout->addWidget(setup_radiobutton(Operation::bilateral_differential_backup, i18nc("@option:radio", "Bilateral differential backup"), QStringLiteral("distribute-horizontal"),
                                                           i18nc("@info:tooltip", "Source directories and their respective destinations will supplement each other with missing files.\nOld files will be overwritten.")));
    operation_groupbox_layout->addWidget(setup_radiobutton(Operation::bilateral_incremental_backup, i18nc("@option:radio", "Bilateral incremental backup"), QStringLiteral("document-save-all"),
                                                           i18nc("@info:tooltip", "Source directories and their respective destinations will supplement each other with missing files.\nOld files will be renamed.")));
    operation_groupbox_layout->addWidget(setup_radiobutton(Operation::differential_backup, i18nc("@option:radio", "Differential backup"), QStringLiteral("arrow-right-double"),
                                                           i18nc("@info:tooltip", "Source directories will supplement their respective destinations with missing files. Old files will be overwritten.")));
    operation_groupbox_layout->addWidget(setup_radiobutton(Operation::incremental_backup, i18nc("@option:radio", "Incremental backup"), QStringLiteral("document-save-as"),
                                                           i18nc("@info:tooltip", "Source directories will supplement their respective destinations with missing files. Old files will be renamed.")));
    operation_groupbox_layout->addWidget(setup_radiobutton(Operation::simple_backup, i18nc("@option:radio", "Simple backup"), QStringLiteral("archive-insert"),
                                                           i18nc("@info:tooltip", "Source directories will be copied into their respective destinations.")));
    operation_groupbox_layout->addWidget(setup_radiobutton(Operation::synchronization, i18nc("@option:radio", "Synchronization"), QStringLiteral("folder-sync"),
                                                           i18nc("@info:tooltip", "Destination directories will be made identical to their respective sources.")));
    radiobutton_map.at(Operation::synchronization)->toggle();
    Q_ASSERT(selected_operation == Operation::synchronization);
    // ↓
    const QString &app_name = app_ptr->applicationDisplayName();
    QGroupBox * const option_groupbox = new QGroupBox(i18nc("@title:group", "Options"));
    QVBoxLayout * const option_groupbox_layout = new QVBoxLayout(option_groupbox);
    SETUP_CHECKBOX(include_hidden_files, QIcon::fromTheme(QStringLiteral("edit-select-all")), i18nc("@info:tooltip", "Hidden files and directories will be taken into account."));
    include_hidden_files_checkbox.setChecked(true);
    SETUP_CHECKBOX(interactive_mode, QIcon::fromTheme(QStringLiteral("view-task")), xi18nc("@info:tooltip", "Before each operation, <application>%1</application> displays what will take place and waits for confirmation.", app_name));
    interactive_mode_checkbox.setChecked(true);
    SETUP_CHECKBOX(log_activity, QIcon::fromTheme(QStringLiteral("text-x-log")), i18nc("@info:tooltip", "You'll find the log file in your home directory."));
    connect(&log_activity_checkbox, &QCheckBox::toggled, this, &KFolderSync::enable_logger, Qt::DirectConnection);
    SETUP_CHECKBOX(quit_when_finished, KStandardGuiItem::quit().icon(), xi18nc("@info:tooltip", "<application>%1</application> will quit as soon as all operations have been completed successfully.", app_name));
    SETUP_CHECKBOX(use_trash, QIcon::fromTheme(QStringLiteral("user-trash-symbolic")), i18nc("@info:tooltip", "Files will be trashed instead of being deleted."));
    // ↓
    QWidget * const central_widget = new QWidget;
    QGridLayout * const central_widget_layout = new QGridLayout(central_widget);
    central_widget_layout->addWidget(&url_requester, 0, 0, 1, 3);
    central_widget_layout->addWidget(operation_groupbox, 1, 0, 1, 1);
    central_widget_layout->addWidget(option_groupbox, 1, 1, 1, 1);
    central_widget_layout->addWidget(&exclusion_widget, 1, 2, 1, 1);
    setCentralWidget(central_widget);

    // Show
    Q_ASSERT([&]{const auto &s = sizeHint(); return s.width() <= 1280 && s.height() <= 768;}());
    setAttribute(Qt::WA_DeleteOnClose);
    show();
}


KFolderSync::Phase1Result KFolderSync::run_phase1(const QUrl &source_url, const QUrl &destination_url)
{
    // Init
    QEventLoop event_loop;
    ProgressDialog progress_dialog(this, i18nc("@title:window", "Retrieving file list"));
    if (logger)
        logger->write(progress_dialog.windowTitle());
    std::map<QString,FileMapEntryInfo> file_map;

    // Get file list
    {
        const QList<QRegularExpression> &pcre_list = exclusion_widget.pcre_list();
        const std::vector<QString> path_to_exclude_vector;
        for (const QUrl& url_to_exclude : exclusion_widget.urls_to_exclude_list()) {
            if (source_url.isParentOf(url_to_exclude))
                const_cast<std::vector<QString>&>(path_to_exclude_vector).push_back(url_to_exclude.path().remove(source_url.path()));
            else if (destination_url.isParentOf(url_to_exclude))
                const_cast<std::vector<QString>&>(path_to_exclude_vector).push_back(url_to_exclude.path().remove(destination_url.path()));
        }
        QPointer<KIO::ListJob> source_listjob, destination_listjob;
        std::mutex mutex;
        SETUP_LISTJOB(source, destination,  +=, file_vs_none, dir_vs_none);
        if (Q_LIKELY(selected_operation != Operation::simple_backup)) {
            SETUP_LISTJOB(destination, source, -=, none_vs_file, none_vs_dir);
        }
        if (Q_UNLIKELY(event_loop.exec() != KIO::ListJob::NoError))
            return Phase1Result();
    }
    file_map.erase(QStringLiteral("."));
    file_map.erase(QStringLiteral(".."));

    // Begin analysis
    progress_dialog.setWindowTitle(i18nc("@title:window", "Analyzing data"));
    progress_dialog.reset(progress_bar_value_t(file_map.size()));
    if (logger) {
        logger->write(progress_dialog.windowTitle());
        connect(&progress_dialog, &ProgressDialog::rejected, [&]{logger->write(KIO::buildErrorString(KIO::ERR_ABORTED, QString()));});
    }
    const bool &use_trash = use_trash_checkbox.isChecked();
    const auto &file_map_end =file_map.cend();
    static const QString &copy_description_template = xi18nc("@label", "Copying...<nl/>source: <filename>%1</filename><nl/>destination: <filename>%2</filename>"),
                         &del_description_template = xi18nc("@label", "Deleting <filename>%1</filename>..."),
                         &mkdir_description_template = xi18nc("@label", "Making <filename>%1</filename>..."),
                         &overwrite_description_template = xi18nc("@label", "Overwriting...<nl/>source: <filename>%1</filename><nl/>destination: <filename>%2</filename>"),
                         &rename_description_template = xi18nc("@label", "Renaming...<nl/>source: <filename>%1</filename><nl/>destination: <filename>%2</filename>"),
                         &trash_description_template = xi18nc("@label", "Trashing <filename>%1</filename>...");
    static constexpr KIO::JobFlag default_kiojob_flags = KIO::HideProgressInfo;
    std::deque<std::function<void(PreviewDialog&)>> preview_dialog_function_deque;
    Phase1Result result(false);
    switch (selected_operation) {
    case Operation::bilateral_differential_backup:
    {
        LOOP_FILE_MAP {
            switch(file_map_entry->second.match) {
            case FileMapEntryInfo::file_vs_file:
            {
                if (file_map_entry->second.relative_time > 0) {
                    OVERWRITE_FILE(source, destination);
                } else if (file_map_entry->second.relative_time < 0) {
                    OVERWRITE_FILE(destination, source);
                }
            } break;
            case FileMapEntryInfo::dir_vs_dir:
                break;
            case FileMapEntryInfo::file_vs_none:
            {
                COPY_FILE(source, destination);
            } break;
            case FileMapEntryInfo::dir_vs_none:
            {
                MKDIR(destination);
            } break;
            case FileMapEntryInfo::none_vs_file:
            {
                COPY_FILE(destination, source);
            } break;
            case FileMapEntryInfo::none_vs_dir:
            {
                MKDIR(source);
            } break;
            case FileMapEntryInfo::dir_vs_file:
            {
                if (file_map_entry->second.relative_time < 0) {
                    REPLACE_DIR_WITH_FILE(source, destination);
                } else {
                    REPLACE_FILE_WITH_DIR(destination);
                }
            } break;
            case FileMapEntryInfo::file_vs_dir:
            {
                if (file_map_entry->second.relative_time < 0) {
                    REPLACE_FILE_WITH_DIR(source);
                } else {
                    REPLACE_DIR_WITH_FILE(destination, source);
                }
            } break;
            default:
                FATAL_BUG;
            }
            UPDATE_PROGRESS_DIALOG;
        }
    } break;
    case Operation::bilateral_incremental_backup:
    {
        LOOP_FILE_MAP {
            switch(file_map_entry->second.match) {
            case FileMapEntryInfo::file_vs_file:
            {
                if (file_map_entry->second.relative_time > 0) {
                    RENAME_AND_COPY_FILE(source, destination);
                } else if (file_map_entry->second.relative_time < 0) {
                    RENAME_AND_COPY_FILE(destination, source);
                }
            } break;
            case FileMapEntryInfo::dir_vs_dir:
                break;
            case FileMapEntryInfo::file_vs_none:
            {
                COPY_FILE(source, destination);
            } break;
            case FileMapEntryInfo::dir_vs_none:
            {
                MKDIR(destination);
            } break;
            case FileMapEntryInfo::none_vs_file:
            {
                COPY_FILE(destination, source);
            } break;
            case FileMapEntryInfo::none_vs_dir:
            {
                MKDIR(source);
            } break;
            case FileMapEntryInfo::dir_vs_file:
            {
                if (file_map_entry->second.relative_time < 0) {
                    RENAME_AND_COPY_FILE(destination, source);
                } else {
                    RENAME_AND_MKDIR(destination);
                }
            } break;
            case FileMapEntryInfo::file_vs_dir:
            {
                if (file_map_entry->second.relative_time < 0) {
                    RENAME_AND_MKDIR(source);
                } else {
                    RENAME_AND_COPY_FILE(source, destination);
                }
            } break;
            default:
                FATAL_BUG;
            }
            UPDATE_PROGRESS_DIALOG;
        }
    } break;
    case Operation::differential_backup:
    {
        LOOP_FILE_MAP {
            switch(file_map_entry->second.match) {
            case FileMapEntryInfo::file_vs_file:
            {
                if (file_map_entry->second.relative_time > 0) {
                    OVERWRITE_FILE(source, destination);
                }
            } break;
            case FileMapEntryInfo::dir_vs_dir:
                break;
            case FileMapEntryInfo::file_vs_none:
            {
                COPY_FILE(source, destination);
            } break;
            case FileMapEntryInfo::dir_vs_none:
            {
                MKDIR(destination);
            } break;
            case FileMapEntryInfo::none_vs_file:
            case FileMapEntryInfo::none_vs_dir:
                break;
            case FileMapEntryInfo::dir_vs_file:
            {
                if (file_map_entry->second.relative_time < 0) {
                    DISCARD_DIR_CONTENTS;
                } else {
                    REPLACE_FILE_WITH_DIR(destination);
                }
            } break;
            case FileMapEntryInfo::file_vs_dir:
            {
                if (file_map_entry->second.relative_time >= 0) {
                    REPLACE_DIR_WITH_FILE(destination, source);
                }
            } break;
            default:
                FATAL_BUG;
            }
            UPDATE_PROGRESS_DIALOG;
        }
    } break;
    case Operation::incremental_backup:
    {
        LOOP_FILE_MAP {
            switch(file_map_entry->second.match) {
            case FileMapEntryInfo::file_vs_file:
            {
                if (file_map_entry->second.relative_time > 0) {
                    RENAME_AND_COPY_FILE(source, destination);
                }
            } break;
            case FileMapEntryInfo::dir_vs_dir:
                break;
            case FileMapEntryInfo::file_vs_none:
            {
                COPY_FILE(source, destination);
            } break;
            case FileMapEntryInfo::dir_vs_none:
            {
                MKDIR(destination);
            } break;
            case FileMapEntryInfo::none_vs_file:
            case FileMapEntryInfo::none_vs_dir:
                break;
            case FileMapEntryInfo::dir_vs_file:
            {
                if (file_map_entry->second.relative_time < 0) {
                    DISCARD_DIR_CONTENTS;
                } else {
                    RENAME_AND_MKDIR(destination);
                }
            } break;
            case FileMapEntryInfo::file_vs_dir:
            {
                if (file_map_entry->second.relative_time >= 0) {
                    RENAME_AND_COPY_FILE(source, destination);
                }
            } break;
            default:
                FATAL_BUG;
            }
            UPDATE_PROGRESS_DIALOG;
        }
    } break;
    case Operation::simple_backup:
    {
        result.task_deque.emplace_back(std::bind(KIO::mkdir, destination_url, -1), mkdir_description_template.arg(destination_url.toDisplayString(URL_DISPLAY_FORMAT)));
        LOOP_FILE_MAP {
            switch(file_map_entry->second.match) {
            case FileMapEntryInfo::file_vs_none:
            {
                COPY_FILE(source, destination);
            } break;
            case FileMapEntryInfo::dir_vs_none:
            {
                MKDIR(destination);
            } break;
            default:
                FATAL_BUG;
            }
            UPDATE_PROGRESS_DIALOG;
        }
    } break;
    case Operation::synchronization:
    {
        LOOP_FILE_MAP {
            switch(file_map_entry->second.match) {
            case FileMapEntryInfo::file_vs_file:
            {
                if (file_map_entry->second.relative_time != 0) {
                    OVERWRITE_FILE(source, destination);
                }
            } break;
            case FileMapEntryInfo::dir_vs_dir:
                break;
            case FileMapEntryInfo::file_vs_none:
            {
                COPY_FILE(source, destination);
            } break;
            case FileMapEntryInfo::dir_vs_none:
            {
                MKDIR(destination);
            } break;
            case FileMapEntryInfo::none_vs_file:
            {
                const QUrl &url = init_child_url(destination_url, file_map_entry->first);
                if (use_trash)
                    result.task_deque.emplace_back(std::bind<KIO::CopyJob*(const QUrl&, KIO::JobFlags)>(KIO::trash, url, default_kiojob_flags), trash_description_template.arg(url.toDisplayString(URL_DISPLAY_FORMAT)));
                else
                    result.task_deque.emplace_back(std::bind(KIO::file_delete, url, default_kiojob_flags), del_description_template.arg(url.toDisplayString(URL_DISPLAY_FORMAT)));
                preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_del_in_destination, std::placeholders::_1,
                                                                  icon_for(url), file_map_entry->first, file_map_entry->second.size_in_destination));
            } break;
            case FileMapEntryInfo::none_vs_dir:
            {
                const QUrl &url = init_child_url(destination_url, file_map_entry->first);
                if (use_trash)
                    result.task_deque.emplace_back(std::bind<KIO::CopyJob*(const QUrl&, KIO::JobFlags)>(KIO::trash, url, default_kiojob_flags), trash_description_template.arg(url.toDisplayString(URL_DISPLAY_FORMAT)));
                else
                    result.task_deque.emplace_back(std::bind<KIO::DeleteJob*(const QUrl&, KIO::JobFlags)>(KIO::del, url, default_kiojob_flags), del_description_template.arg(url.toDisplayString(URL_DISPLAY_FORMAT)));
                KIO::filesize_t size = file_map_entry->second.size_in_destination;
                SUBLOOP_FILE_MAP {
                    if (Q_LIKELY(next_file_map_entry->first.startsWith(file_map_entry->first)))
                        size += next_file_map_entry->second.size_in_destination;
                    else
                        break;
                }
                preview_dialog_function_deque.push_back(std::bind(&PreviewDialog::add_del_in_destination, std::placeholders::_1, icon_for(url), file_map_entry->first, size));
            } break;
            case FileMapEntryInfo::dir_vs_file:
            {
                REPLACE_FILE_WITH_DIR(destination);
            } break;
            case FileMapEntryInfo::file_vs_dir:
            {
                REPLACE_DIR_WITH_FILE(destination, source);
            } break;
            default:
                FATAL_BUG;
            }
            UPDATE_PROGRESS_DIALOG;
        }
    } break;
    }

    // Show preview
    if (interactive_mode_checkbox.isChecked()) {
        const auto &preview_dialog_function_deque_size = preview_dialog_function_deque.size();
        if (Q_UNLIKELY(preview_dialog_function_deque_size == 0)) {
            progress_dialog.hide();
            KMessageBox::information(this, i18nc("@label", "Nothing to do."));
        } else {
            progress_dialog.setWindowTitle(i18nc("@label", "Creating preview"));
            progress_dialog.reset(progress_bar_value_t(preview_dialog_function_deque_size));
            PreviewDialog preview_dialog(this, source_url.toDisplayString(URL_DISPLAY_FORMAT), destination_url.toDisplayString(URL_DISPLAY_FORMAT), PreviewDialog::table_row_t(preview_dialog_function_deque_size));
            for (const auto &function : preview_dialog_function_deque) {
                function(preview_dialog);
                UPDATE_PROGRESS_DIALOG;
            }
            progress_dialog.hide();
            const auto &preview_dialog_result = preview_dialog.exec();
            if (Q_UNLIKELY(preview_dialog_result != QDialog::Accepted)) {
                if (logger)
                    logger->write(KIO::buildErrorString(KIO::ERR_ABORTED, QString()));
                return Phase1Result();
            }
        }
    }

    return result;
}


inline QRadioButton * KFolderSync::setup_radiobutton(const Operation &&operation, const QString &&text, const QString &&icon_name, const QString &&tooltip)
{
    QRadioButton * const radiobutton = const_cast<std::map<Operation,QRadioButton*>&>(radiobutton_map)[operation] = new QRadioButton(text);
    radiobutton->setIcon(QIcon::fromTheme(icon_name));
    radiobutton->setToolTip(tooltip);
    connect(radiobutton, &QRadioButton::clicked, [=]{selected_operation = operation;});
    return radiobutton;
}


void KFolderSync::enable_logger(const bool &b)
{
    if (b) {
        Q_ASSERT(logger.isNull());
        logger = new Logger(this);
        const QString &file_name = QDir::homePath() % QStringLiteral("/(%1) - ").arg(qApp->applicationDisplayName()) % QDateTime::currentDateTime().toString(Qt::SystemLocaleLongDate) % QStringLiteral(".log");
        if (Q_UNLIKELY(logger->open_file(file_name) == false)) {
            qobject_cast<QCheckBox*>(sender())->setChecked(false);
            KMessageBox::sorry(this, xi18nc("@info", "I can't create <filename>%1</filename>...<nl/>Please check your permissions.", file_name));
        }
    } else delete logger;
}


void KFolderSync::manage_profiles()
{
    constexpr struct {
        const char *destination_urls = "D",
                   *include_hidden_files = "IHF",
                   *log_activity = "LA",
                   *operation = "OT",
                   *interactive_mode = "IM",
                   *pcre_pattern_list = "PCREPL",
                   *quit_when_finished = "QWF",
                   *source_urls = "S",
                   *urls_to_exclude_list = "UEL",
                   *use_trash = "UT";
        // NEVER change these values or existing profiles will stop working!
    } keys;

    // Ask
    ProfileDialog profile_dialog(this);
    const auto &config = KSharedConfig::openConfig(QStringLiteral("profiles"), KConfig::SimpleConfig, QStandardPaths::AppConfigLocation);
    const auto &config_group_list = config->groupList();
    bool ok = false;

    // Exec
    QString config_group_name;
    switch (profile_dialog.exec()) {
    case ProfileDialog::load_profile:
    {
        if (Q_UNLIKELY(config_group_list.isEmpty()))
            break;
        config_group_name = QInputDialog::getItem(this, QString(), i18nc("@label", "Select the profile you want to load:"), config_group_list, 0, false, &ok);
        if (Q_LIKELY(ok)) {
            KConfigGroup config_group(config, config_group_name);
            // options
            include_hidden_files_checkbox.setChecked(config_group.readEntry(keys.include_hidden_files, include_hidden_files_checkbox.isChecked()));
            interactive_mode_checkbox.setChecked(config_group.readEntry(keys.interactive_mode, interactive_mode_checkbox.isChecked()));
            log_activity_checkbox.setChecked(config_group.readEntry(keys.log_activity, log_activity_checkbox.isChecked()));
            quit_when_finished_checkbox.setChecked(config_group.readEntry(keys.quit_when_finished, quit_when_finished_checkbox.isChecked()));
            use_trash_checkbox.setChecked(config_group.readEntry(keys.use_trash, use_trash_checkbox.isChecked()));
            // operation type
            radiobutton_map.at(Operation(config_group.readEntry(keys.operation, int(selected_operation))))->click();
            // operation url list
            url_requester.restore(config_group.readEntry(keys.source_urls, QList<QUrl>()), config_group.readEntry(keys.destination_urls, QList<QUrl>()));
            // urls to exclude and pcres
            exclusion_widget.restore(config_group.readEntry(keys.pcre_pattern_list, QList<QString>()), config_group.readEntry(keys.urls_to_exclude_list, QList<QUrl>()));
        }
    } return;
    case ProfileDialog::save_profile:
    {
        Q_FOREVER {
            config_group_name = QInputDialog::getText(this, QString(), KStandardGuiItem::saveAs().text(), QLineEdit::Normal, i18nc("@item", "Profile %1", config_group_list.size()+1), &ok);
            if (Q_UNLIKELY(ok == false))
                return;
            if (Q_UNLIKELY(config_group_list.contains(config_group_name))) {
                if (KMessageBox::questionYesNo(this, xi18nc("@info", "<resource>%1</resource> already exists. Do you want to overwrite it?", config_group_name)) == KMessageBox::Yes) {
                    config->deleteGroup(config_group_name);
                    break;
                }
            } else break;
        }
        KConfigGroup config_group(config, config_group_name);
        // options
        config_group.writeEntry(keys.include_hidden_files, include_hidden_files_checkbox.isChecked());
        config_group.writeEntry(keys.interactive_mode, interactive_mode_checkbox.isChecked());
        config_group.writeEntry(keys.log_activity, log_activity_checkbox.isChecked());
        config_group.writeEntry(keys.quit_when_finished, quit_when_finished_checkbox.isChecked());
        config_group.writeEntry(keys.use_trash, use_trash_checkbox.isChecked());
        // operation type
        config_group.writeEntry(keys.operation, int(selected_operation));
        // operation url list
        for (const auto &urls : url_requester.get_operation_url_list()) {
            config_group.writeEntry(keys.source_urls, urls[0]);
            config_group.writeEntry(keys.destination_urls, urls[1]);
        }
        // urls to exclude and pcres
        config_group.writeEntry(keys.pcre_pattern_list, [this]{QList<QString> list; for (const auto& pcre : exclusion_widget.pcre_list()) list.append(pcre.pattern()); return list;}());
        config_group.writeEntry(keys.urls_to_exclude_list, exclusion_widget.urls_to_exclude_list());

    } return;
    case ProfileDialog::delete_profile:
    {
        if (Q_UNLIKELY(config_group_list.isEmpty()))
            break;
        config_group_name = QInputDialog::getItem(this, QString(), i18nc("@label", "Select the profile you want to delete:"), config_group_list, 0, false, &ok);
        if (Q_LIKELY(ok))
            config->deleteGroup(config_group_name);
    } return;
    case ProfileDialog::import_profile:
    {
        const QUrl &url = QFileDialog::getOpenFileUrl(this);
        if (Q_LIKELY(url.isEmpty() == false)) {
            KIO::file_copy(url, QUrl::fromLocalFile(QStandardPaths::locate(config->locationType(), config->name())), -1, KIO::Overwrite);
            config->reparseConfiguration();
        }
    } return;
    case ProfileDialog::export_profile:
    {
        if (Q_UNLIKELY(config_group_list.isEmpty()))
            break;
        const QUrl &url = QFileDialog::getSaveFileUrl(this);
        if (Q_LIKELY(url.isEmpty() == false))
            KIO::file_copy(QUrl::fromLocalFile(QStandardPaths::locate(config->locationType(), config->name())), url, -1, KIO::Overwrite);
    } return;
    case QDialog::Rejected:
        return;
    default:
        FATAL_BUG;
    }

    // Show error
    KMessageBox::sorry(this, i18nc("@info", "There are no saved profiles..."));
}


void KFolderSync::run()
{
    // Check and setup
    const auto &number_of_operations = url_requester.get_operation_url_list().size();
    if (Q_UNLIKELY(number_of_operations < 1)) {
        KMessageBox::sorry(this, i18nc("@info", "You haven't added the directories to work with yet..."));
        return;
    }
    if (logger)
        logger->write(i18nc("@info:progress", "*** Starting ***\nOperation type: %1.\nNumber of operations to carry out: %2.",
                            radiobutton_map.at(selected_operation)->text().remove(QLatin1Char('&')).toLower(), number_of_operations));

    // Perform operations sequentially
    for (std::decay<decltype(number_of_operations)>::type o = 0; Q_LIKELY(o < number_of_operations); ++o) {
        const QUrl &source_url = url_requester.get_operation_url_list()[o][0];
        const QUrl &destination_url = Q_LIKELY(selected_operation != Operation::simple_backup) ? url_requester.get_operation_url_list()[o][1] :
                init_child_url(url_requester.get_operation_url_list()[o][1], xi18nc("@item new folder name", "Backup of <filename>%1</filename> (%2)",
                                                                                    source_url.adjusted/*mandatory*/(QUrl::StripTrailingSlash).fileName(),
                                                                                    QDateTime::currentDateTime().toString(Qt::SystemLocaleLongDate)) % QLatin1Char('/'));
        // phase 1/2: retrieve file list, analyze it and show a preview
        const Phase1Result &phase1_result = run_phase1(source_url, destination_url);
        if (Q_UNLIKELY(phase1_result.error)) {
            return;
        }
        // phase 2/2: execute tasks (kio jobs)
        TaskProgressDialog task_progress_dialog(this, progress_bar_value_t(phase1_result.task_deque.size()), i18nc("@title:window", "Operation %1/%2", o+1, number_of_operations));
        QEventLoop event_loop;
        for (const auto &task : phase1_result.task_deque) {
            QPointer<KIO::Job> kiojob = task.first();
            kiojob->setUiDelegateExtension(nullptr);
            kiojob->uiDelegate()->setAutoErrorHandlingEnabled(true);
            task_progress_dialog.register_new_kiojob(kiojob, task.second);
            if (logger) {
                connect(kiojob, &KIO::Job::infoMessage, logger, &Logger::log_kiojob_info, Qt::DirectConnection);
                connect(kiojob, &KIO::Job::warning, logger, &Logger::log_kiojob_info, Qt::DirectConnection);
                logger->write(task.second);
            }
            connect(kiojob, &KIO::Job::result, [&]{
                const auto &error = kiojob->error();
                if (Q_UNLIKELY(error != KIO::ListJob::NoError) && logger) {
                    const QString &error_string = kiojob->errorString();
                    logger->write(error_string.isEmpty() ? kiojob->detailedErrorStrings().first() : error_string);
                }
                event_loop.exit(error);
            });
            if (Q_UNLIKELY(event_loop.exec() != KIO::Job::NoError))
                return;
        }
    }

    // Finish
    const auto &msg = i18nc("@info:progress", "*** Mission accomplished! ***");
    if (logger)
        logger->write(msg);
    if (quit_when_finished_checkbox.isChecked()) {
        qApp->exit(EXIT_SUCCESS);
        return;
    }
    KMessageBox::information(this, msg);
}
