/*
 * Fooyin
 * Copyright © 2023, Luke Taylor <LukeT1@proton.me>
 *
 * Fooyin 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
 * (at your option) any later version.
 *
 * Fooyin 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 Fooyin.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "guiapplication.h"

#include "artwork/artworkdialog.h"
#include "artwork/artworkfinder.h"
#include "artwork/artworksaveutils.h"
#include "dialog/autoplaylistdialog.h"
#include "dialog/saveplaylistsdialog.h"
#include "dialog/searchdialog.h"
#include "internalguisettings.h"
#include "mainwindow.h"
#include "menubar/editmenu.h"
#include "menubar/filemenu.h"
#include "menubar/helpmenu.h"
#include "menubar/layoutmenu.h"
#include "menubar/librarymenu.h"
#include "menubar/mainmenubar.h"
#include "menubar/playbackmenu.h"
#include "menubar/viewmenu.h"
#include "playlist/manager/playlistmanagerwidget.h"
#include "playlist/playlistcontroller.h"
#include "playlist/playlistinteractor.h"
#include "playlist/playlistuicontroller.h"
#include "queueviewer/queueviewer.h"
#include "scripting/scriptcommandhandler.h"
#include "scripting/scriptvariableproviders.h"
#include "search/searchcontroller.h"
#include "search/searchwidget.h"
#include "statusevent.h"
#include "systemtrayicon.h"
#include "widgets.h"

#include <core/application.h>
#include <core/corepaths.h>
#include <core/coresettings.h>
#include <core/database/database.h>
#include <core/engine/enginehandler.h>
#include <core/engine/enginehelpers.h>
#include <core/internalcoresettings.h>
#include <core/library/librarymanager.h>
#include <core/library/musiclibrary.h>
#include <core/playlist/playlisthandler.h>
#include <core/playlist/playlistloader.h>
#include <core/playlist/playlistparser.h>
#include <core/plugins/plugininfo.h>
#include <core/plugins/pluginmanager.h>
#include <core/scripting/scriptenvironmenthelpers.h>
#include <gui/coverprovider.h>
#include <gui/editablelayout.h>
#include <gui/guiconstants.h>
#include <gui/guisettings.h>
#include <gui/guiutils.h>
#include <gui/iconloader.h>
#include <gui/layoutprovider.h>
#include <gui/plugins/dspguiplugin.h>
#include <gui/plugins/guiplugin.h>
#include <gui/plugins/guiplugincontext.h>
#include <gui/plugins/pluginconfigguiplugin.h>
#include <gui/propertiesdialog.h>
#include <gui/scripting/scripteditor.h>
#include <gui/theme/fytheme.h>
#include <gui/theme/themeregistry.h>
#include <gui/trackselectioncontroller.h>
#include <gui/widgetprovider.h>
#include <gui/widgets/elapsedprogressdialog.h>
#include <gui/windowcontroller.h>
#include <utils/actions/actioncontainer.h>
#include <utils/actions/actionmanager.h>
#include <utils/actions/command.h>
#include <utils/audioutils.h>
#include <utils/fileutils.h>
#include <utils/logging/logwidget.h>
#include <utils/settings/advancedsettingsregistry.h>
#include <utils/settings/settingsdialogcontroller.h>
#include <utils/settings/settingsmanager.h>
#include <utils/utils.h>

#include <QAction>
#include <QApplication>
#include <QCheckBox>
#include <QFileDialog>
#include <QFileInfo>
#include <QImageReader>
#include <QLoggingCategory>
#include <QMessageBox>
#include <QMimeDatabase>
#include <QPixmapCache>
#include <QPointer>
#include <QPushButton>
#include <QStyle>
#include <QStyleFactory>
#include <QTimer>

Q_LOGGING_CATEGORY(GUI_APP, "fy.gui")

using namespace std::chrono_literals;
using namespace Qt::StringLiterals;

namespace Fooyin {
namespace {
QString pluginIdentifierForRoot(const PluginManager* pluginManager, const QObject* root)
{
    if(!pluginManager || !root) {
        return {};
    }

    for(const auto& [pluginId, pluginInfo] : pluginManager->allPluginInfo()) {
        if(pluginInfo && pluginInfo->root() == root) {
            return pluginId;
        }
    }

    return {};
}

std::pair<QString, QString> resolveIconThemes(SettingsManager* settings)
{
    if(!settings) {
        return {};
    }

    QString primaryTheme;
    QString fallbackTheme;

    const auto iconTheme = static_cast<IconThemeOption>(settings->value<Settings::Gui::IconTheme>());
    switch(iconTheme) {
        case IconThemeOption::AutoDetect:
            primaryTheme = Utils::isDarkMode() ? QString::fromLatin1(Constants::DarkIconTheme)
                                               : QString::fromLatin1(Constants::LightIconTheme);
            break;
        case IconThemeOption::Light:
            primaryTheme = QString::fromLatin1(Constants::LightIconTheme);
            break;
        case IconThemeOption::Dark:
            primaryTheme = QString::fromLatin1(Constants::DarkIconTheme);
            break;
        case IconThemeOption::System:
            fallbackTheme = Utils::isDarkMode() ? QString::fromLatin1(Constants::DarkIconTheme)
                                                : QString::fromLatin1(Constants::LightIconTheme);
            break;
    }

    return {primaryTheme, fallbackTheme};
}

void applyInitialIconThemes(SettingsManager* settings)
{
    const auto [primaryTheme, fallbackTheme] = resolveIconThemes(settings);
    Gui::setThemeIconOverrides(primaryTheme, fallbackTheme);
}
} // namespace

class GuiApplicationPrivate
{
public:
    explicit GuiApplicationPrivate(GuiApplication* self_, Application* core_);

    void initialise();
    void setupConnections();
    void initialisePlugins();

    void showPluginsNotFoundMessage();
    void initialiseTray();
    void updateWindowTitle();
    void checkArtwork();
    void handleTrackStatus(const Engine::TrackStatusContext& context);
    void handleTracksDeleted(const TrackList& tracks);

    void removeExpiredCovers(const TrackList& tracks);

    void registerActions();
    void rescanTracks(const TrackList& tracks, bool onlyModified) const;

    void setupScanMenu();
    void setupRatingMenu();
    void setupUtilitiesMenu();

    void close();
    void changeVolume(double delta) const;
    void mute() const;
    void setStyle() const;
    void setTheme() const;
    bool setIconTheme() const;
    void refreshAutoDetectedIconTheme() const;
    void registerLayouts();

    void checkTracksNeedUpdate() const;
    void showNeedReloadMessage() const;

    void showScriptEditor();
    void showSearchPlaylistDialog();
    void showSearchLibraryDialog();
    void showPlaybackQueue();
    void showPlaylistManager();
    void focusSearchBar() const;
    void showQuickSearch() const;
    void showPropertiesDialog(const TrackList& tracks) const;
    void showEngineError(const QString& error) const;
    void showMessage(const QString& title, const Track& track) const;
    void showTrackNotFoundMessage(const Track& track) const;
    void showTrackUnreableMessage(const Track& track) const;

    void createNewPlaylist() const;
    void createNewAutoPlaylist();

    void addFiles();
    void addFolders();
    void openFiles(const QList<QUrl>& urls);

    void loadPlaylist();
    void savePlaylist() const;
    void savePlaylist(const Playlist* playlist) const;
    void saveAllPlaylist() const;

    GuiApplication* m_self;
    Application* m_core;

    SettingsManager* m_settings;
    ActionManager* m_actionManager;
    MusicLibrary* m_library;
    PlayerController* m_playerController;
    PlaylistHandler* m_playlistHandler;

    WidgetProvider m_widgetProvider;
    GuiSettings m_guiSettings;
    LayoutProvider m_layoutProvider;
    std::unique_ptr<EditableLayout> m_editableLayout;
    std::unique_ptr<MainMenuBar> m_menubar;
    std::unique_ptr<MainWindow> m_mainWindow;
    std::unique_ptr<SystemTrayIcon> m_trayIcon;
    WidgetContext* m_mainContext;
    std::unique_ptr<PlaylistController> m_playlistController;
    PlaylistInteractor m_playlistInteractor;
    TrackSelectionController m_selectionController;
    SearchController* m_searchController;

    FileMenu* m_fileMenu;
    EditMenu* m_editMenu;
    ViewMenu* m_viewMenu;
    LayoutMenu* m_layoutMenu;
    PlaybackMenu* m_playbackMenu;
    LibraryMenu* m_libraryMenu;
    HelpMenu* m_helpMenu;

    PropertiesDialog* m_propertiesDialog;
    std::unique_ptr<ScriptCommandHandler> m_scriptCommandHandler;
    WindowController* m_windowController;
    ThemeRegistry* m_themeRegistry;
    std::unique_ptr<AdvancedSettingsRegistry> m_advancedSettingsRegistry;

    GuiPluginContext m_guiPluginContext;

    std::unique_ptr<LogWidget> m_logWidget;
    QPointer<QueueViewer> m_playbackQueueWidget;
    QPointer<PlaylistManagerWidget> m_playlistManagerWidget;
    Widgets* m_widgets;
    ScriptParser m_scriptParser;
    CoverProvider m_coverProvider;
};

namespace {
std::unique_ptr<GuiApplicationPrivate> createGuiApplicationPrivate(GuiApplication* self, Application* core)
{
    // Some widgets and actions resolve icons during GuiApplicationPrivate construction,
    // so the icon theme overrides must be set up before creating it
    applyInitialIconThemes(core ? core->settingsManager() : nullptr);
    return std::make_unique<GuiApplicationPrivate>(self, core);
}
} // namespace

GuiApplicationPrivate::GuiApplicationPrivate(GuiApplication* self_, Application* core_)
    : m_self{self_}
    , m_core{core_}
    , m_settings{m_core->settingsManager()}
    , m_actionManager{new ActionManager(m_settings, m_self)}
    , m_library{m_core->library()}
    , m_playerController{m_core->playerController()}
    , m_playlistHandler{m_core->playlistHandler()}
    , m_guiSettings{m_settings}
    , m_editableLayout{std::make_unique<EditableLayout>(m_actionManager, &m_widgetProvider, &m_layoutProvider,
                                                        m_settings)}
    , m_menubar{std::make_unique<MainMenuBar>(m_actionManager, m_settings)}
    , m_mainWindow{std::make_unique<MainWindow>(m_actionManager, m_menubar.get(), m_library, m_settings)}
    , m_mainContext{new WidgetContext(m_mainWindow.get(), Context{"Fooyin.MainWindow"}, m_self)}
    , m_playlistController{std::make_unique<PlaylistController>(m_core, &m_selectionController)}
    , m_playlistInteractor{m_core->playlistHandler(), m_playlistController.get(), m_library, m_settings}
    , m_selectionController{m_actionManager, m_core->audioLoader().get(), m_settings, m_playlistController.get()}
    , m_searchController{new SearchController(m_editableLayout.get(), m_self)}
    , m_fileMenu{new FileMenu(m_actionManager, m_settings, m_self)}
    , m_editMenu{new EditMenu(m_actionManager, m_settings, m_self)}
    , m_viewMenu{new ViewMenu(m_actionManager, m_settings, m_self)}
    , m_layoutMenu{new LayoutMenu(m_actionManager, &m_layoutProvider, m_settings, m_self)}
    , m_playbackMenu{new PlaybackMenu(m_actionManager, m_playerController, m_settings, m_self)}
    , m_libraryMenu{new LibraryMenu(m_core, m_actionManager, m_self)}
    , m_helpMenu{new HelpMenu(m_actionManager, m_self)}
    , m_propertiesDialog{new PropertiesDialog(m_actionManager, m_settings, m_self)}
    , m_scriptCommandHandler{std::make_unique<ScriptCommandHandler>(m_actionManager, m_playerController,
                                                                    m_propertiesDialog)}
    , m_windowController{new WindowController(m_mainWindow.get())}
    , m_themeRegistry{new ThemeRegistry(m_settings, m_self)}
    , m_advancedSettingsRegistry{std::make_unique<AdvancedSettingsRegistry>(m_settings)}
    , m_guiPluginContext{m_actionManager,
                         &m_layoutProvider,
                         &m_selectionController,
                         m_searchController,
                         m_propertiesDialog,
                         m_scriptCommandHandler.get(),
                         &m_widgetProvider,
                         m_editableLayout.get(),
                         m_windowController,
                         m_themeRegistry,
                         m_advancedSettingsRegistry.get()}
    , m_logWidget{std::make_unique<LogWidget>(m_settings)}
    , m_widgets{new Widgets(m_core, m_mainWindow.get(), m_self, &m_playlistInteractor, m_scriptCommandHandler.get(),
                            m_self)}
    , m_coverProvider{m_core->audioLoader(), m_settings}
{
    m_scriptParser.addProvider(playlistVariableProvider());
}

void GuiApplicationPrivate::initialise()
{
    qApp->installEventFilter(m_self);

    setupConnections();
    registerActions();
    setupScanMenu();
    setupRatingMenu();
    setupUtilitiesMenu();
    setStyle();
    setIconTheme();
    registerLayouts();

    QImageReader::setAllocationLimit(m_settings->value<Settings::Gui::Internal::ImageAllocationLimit>());

    m_widgets->registerWidgets();
    m_widgets->registerPages();
    m_widgets->registerAdvancedSettings();
    m_widgets->registerDspSettings();
    m_widgets->registerPropertiesTabs();
    m_widgets->registerFontEntries();

    m_actionManager->addContextObject(m_mainContext);

    initialisePlugins();

    m_widgets->registerDspWidgets();
    m_viewMenu->registerDspSettingsActions(m_widgets->dspSettingsRegistry(), m_widgets->dspSettingsController());
    m_layoutProvider.findLayouts();
    m_layoutMenu->setup();
    m_editableLayout->initialise();
    m_mainWindow->setCentralWidget(m_editableLayout.get());
    m_searchController->loadWidgets();

    auto openMainWindow = [this]() {
        m_mainWindow->open();
        if(m_settings->value<Settings::Core::FirstRun>()) {
            QMetaObject::invokeMethod(m_editableLayout.get(), &EditableLayout::showQuickSetup, Qt::QueuedConnection);
        }
        QMetaObject::invokeMethod(m_mainWindow.get(), [this]() { checkTracksNeedUpdate(); }, Qt::QueuedConnection);
    };

    if(m_core->libraryManager()->hasLibrary() && m_core->library()->isEmpty()
       && m_settings->value<Settings::Gui::WaitForTracks>()) {
        QObject::connect(m_core->library(), &MusicLibrary::tracksLoaded, openMainWindow);
    }
    else {
        openMainWindow();
    }

    initialiseTray();
}

void GuiApplicationPrivate::setupConnections()
{
    QObject::connect(m_library, &MusicLibrary::tracksMetadataChanged, m_self,
                     [this](const TrackList& tracks) { removeExpiredCovers(tracks); });
    QObject::connect(m_library, &MusicLibrary::tracksMetadataChanged, &m_selectionController,
                     &TrackSelectionController::tracksUpdated);
    QObject::connect(m_library, &MusicLibrary::tracksUpdated, &m_selectionController,
                     &TrackSelectionController::tracksUpdated);
    QObject::connect(m_library, &MusicLibrary::tracksDeleted, &m_selectionController,
                     &TrackSelectionController::tracksRemoved);
    QObject::connect(m_library, &MusicLibrary::tracksDeleted, m_self,
                     [this](const TrackList& tracks) { handleTracksDeleted(tracks); });

    QObject::connect(m_playerController, &PlayerController::playStateChanged, m_mainWindow.get(),
                     [this]() { updateWindowTitle(); });
    QObject::connect(m_playerController, &PlayerController::playlistTrackUpdated, m_self,
                     [this]() { updateWindowTitle(); });
    QObject::connect(m_playerController, &PlayerController::positionChangedSeconds, m_self,
                     [this]() { updateWindowTitle(); });
    QObject::connect(m_playerController, &PlayerController::trackQueueChanged, m_self,
                     [this]() { updateWindowTitle(); });
    QObject::connect(m_playlistHandler, &PlaylistHandler::activePlaylistChanged, m_self,
                     [this]() { updateWindowTitle(); });

    const auto updateTitleForActivePlaylist = [this](Playlist* playlist) {
        if(playlist == m_playlistHandler->activePlaylist()) {
            updateWindowTitle();
        }
    };
    QObject::connect(m_playlistHandler, &PlaylistHandler::tracksAdded, m_self, updateTitleForActivePlaylist);
    QObject::connect(m_playlistHandler, &PlaylistHandler::tracksChanged, m_self, updateTitleForActivePlaylist);
    QObject::connect(m_playlistHandler, &PlaylistHandler::tracksUpdated, m_self, updateTitleForActivePlaylist);
    QObject::connect(m_playlistHandler, &PlaylistHandler::tracksRemoved, m_self, updateTitleForActivePlaylist);

    m_settings->subscribe<Settings::Gui::Internal::WindowTitleTrackScript>(m_self, [this]() { updateWindowTitle(); });
    m_settings->subscribe<Settings::Gui::RatingFullStarSymbol>(m_self, [this](const QString&) { updateWindowTitle(); });
    m_settings->subscribe<Settings::Gui::RatingHalfStarSymbol>(m_self, [this](const QString&) { updateWindowTitle(); });
    m_settings->subscribe<Settings::Gui::RatingEmptyStarSymbol>(m_self,
                                                                [this](const QString&) { updateWindowTitle(); });

    QObject::connect(m_playerController, &PlayerController::currentTrackChanged, m_self,
                     [this]() { updateWindowTitle(); });
    QObject::connect(&m_selectionController, &TrackSelectionController::actionExecuted, m_playlistController.get(),
                     &PlaylistController::handleTrackSelectionAction);
    QObject::connect(&m_selectionController, &TrackSelectionController::requestPropertiesDialog, m_self,
                     [this](const TrackList& tracks) { showPropertiesDialog(tracks); });
    QObject::connect(
        &m_selectionController, &TrackSelectionController::requestArtworkSearch, m_self,
        [this](const TrackList& tracks, bool quick) { m_self->searchForArtwork(tracks, Track::Cover::Front, quick); });
    QObject::connect(&m_selectionController, &TrackSelectionController::requestArtworkAttach, m_self,
                     [this](const TrackList& tracks, Track::Cover type, const QString& filepath) {
                         m_self->attachArtwork(tracks, type, filepath);
                     });
    QObject::connect(&m_selectionController, &TrackSelectionController::requestArtworkRemoval, m_self,
                     [this](const TrackList& tracks) { m_self->removeAllArtwork(tracks); });
    QObject::connect(m_fileMenu, &FileMenu::requestNewPlaylist, m_self, [this]() { createNewPlaylist(); });
    QObject::connect(m_fileMenu, &FileMenu::requestNewAutoPlaylist, m_self, [this]() { createNewAutoPlaylist(); });
    QObject::connect(m_fileMenu, &FileMenu::requestExit, m_self, [this]() { close(); });
    QObject::connect(m_fileMenu, &FileMenu::requestAddFiles, m_self, [this]() { addFiles(); });
    QObject::connect(m_fileMenu, &FileMenu::requestAddFolders, m_self, [this]() { addFolders(); });
    QObject::connect(m_fileMenu, &FileMenu::requestLoadPlaylist, m_self, [this]() { loadPlaylist(); });
    QObject::connect(m_fileMenu, &FileMenu::requestSavePlaylist, m_self, [this]() { savePlaylist(); });
    QObject::connect(m_fileMenu, &FileMenu::requestSaveAllPlaylists, m_self, [this]() { saveAllPlaylist(); });
    QObject::connect(m_editMenu, &EditMenu::requestSearch, m_self, [this]() { showSearchPlaylistDialog(); });
    QObject::connect(m_libraryMenu, &LibraryMenu::requestSearch, m_self, [this]() { showSearchLibraryDialog(); });
    QObject::connect(m_libraryMenu, &LibraryMenu::requestQuickSearch, m_self, [this]() { showQuickSearch(); });
    QObject::connect(m_viewMenu, &ViewMenu::openQuickSetup, m_editableLayout.get(), &EditableLayout::showQuickSetup);
    QObject::connect(m_viewMenu, &ViewMenu::openPlaybackQueue, m_self, [this]() { showPlaybackQueue(); });
    QObject::connect(m_viewMenu, &ViewMenu::openPlaylistManager, m_self, [this]() { showPlaylistManager(); });
    QObject::connect(m_viewMenu, &ViewMenu::focusSearchBar, m_self, [this]() { focusSearchBar(); });
    QObject::connect(m_viewMenu, &ViewMenu::openLog, m_logWidget.get(), &LogWidget::show);
    QObject::connect(m_viewMenu, &ViewMenu::openScriptEditor, m_self, [this]() { showScriptEditor(); });
    QObject::connect(m_viewMenu, &ViewMenu::showNowPlaying, m_self, [this]() {
        if(auto* activePlaylist = m_playlistHandler->activePlaylist()) {
            m_playlistController->uiController()->showNowPlaying();
            m_playlistController->changeCurrentPlaylist(activePlaylist);
        }
    });
    QObject::connect(m_layoutMenu, &LayoutMenu::changeLayout, m_editableLayout.get(), &EditableLayout::changeLayout);
    QObject::connect(m_layoutMenu, &LayoutMenu::importLayout, m_self,
                     [this]() { m_layoutProvider.importLayout(m_mainWindow.get()); });
    QObject::connect(m_layoutMenu, &LayoutMenu::exportLayout, m_self,
                     [this]() { m_editableLayout->exportLayout(m_editableLayout.get()); });

    QObject::connect(&m_layoutProvider, &LayoutProvider::requestChangeLayout, m_editableLayout.get(),
                     &EditableLayout::changeLayout);
    QObject::connect(m_editableLayout.get(), &EditableLayout::layoutChanged, m_searchController,
                     &SearchController::loadWidgets);

    QObject::connect(m_core->engine(), &EngineController::engineError, m_self,
                     [this](const QString& error) { showEngineError(error); });
    QObject::connect(m_core->engine(), &EngineController::trackStatusContextChanged, m_self,
                     [this](const Engine::TrackStatusContext& context) { handleTrackStatus(context); });

    m_settings->subscribe<Settings::Gui::LayoutEditing>(m_self, [this]() { updateWindowTitle(); });

    const auto refreshThemedActions = [this]() {
        Gui::refreshThemeIcons(m_actionManager);
        for(const Command* command : m_actionManager->commands()) {
            Gui::refreshThemeIcon(command ? command->action() : nullptr);
        }
        Gui::refreshThemeIcons(m_self);
        const auto widgets = QApplication::topLevelWidgets();
        for(QWidget* widget : widgets) {
            Gui::refreshThemeIcons(widget);
        }
    };

    m_settings->subscribe<Settings::Gui::IconTheme>(m_self, [this, refreshThemedActions]() {
        setIconTheme();
        QPixmapCache::clear();
        refreshThemedActions();
    });
    m_settings->subscribe<Settings::Gui::Theme>(m_self, [this]() {
        setTheme();
        if(setIconTheme()) {
            QPixmapCache::clear();
            m_settings->refresh<Settings::Gui::IconTheme>();
        }
    });
    m_settings->subscribe<Settings::Gui::Style>(m_self, [this]() {
        setStyle();
        setTheme();
        if(setIconTheme()) {
            QPixmapCache::clear();
            m_settings->refresh<Settings::Gui::IconTheme>();
        }
    });

    m_settings->subscribe<Settings::Gui::Internal::ImageAllocationLimit>(m_settings, &QImageReader::setAllocationLimit);
}

void GuiApplicationPrivate::initialisePlugins()
{
    if(m_core->pluginManager()->allPluginInfo().empty()) {
        QMetaObject::invokeMethod(m_self, [this]() { showPluginsNotFoundMessage(); }, Qt::QueuedConnection);
        return;
    }

    m_core->pluginManager()->initialisePlugins<GuiPlugin>(
        [this](GuiPlugin* plugin) { plugin->initialise(m_guiPluginContext); });

    m_core->pluginManager()->initialisePlugins<DspGuiPlugin>([this](DspGuiPlugin* plugin) {
        auto providers = plugin->settingsProviders();
        for(auto& provider : providers) {
            if(provider && m_widgets->dspSettingsRegistry()) {
                m_widgets->dspSettingsRegistry()->registerProvider(std::move(provider));
            }
        }
    });

    m_core->pluginManager()->initialisePlugins<PluginConfigGuiPlugin>([this](PluginConfigGuiPlugin* plugin) {
        auto provider          = plugin->settingsProvider();
        auto* root             = dynamic_cast<QObject*>(plugin);
        const QString pluginId = pluginIdentifierForRoot(m_core->pluginManager(), root);
        if(provider && m_widgets->pluginSettingsRegistry() && !pluginId.isEmpty()) {
            m_widgets->pluginSettingsRegistry()->registerProvider(pluginId, std::move(provider));
        }
    });
}

void GuiApplicationPrivate::showPluginsNotFoundMessage()
{
    QMessageBox message;
    message.setIcon(QMessageBox::Warning);
    message.setText(GuiApplication::tr("Plugins not found"));
    message.setInformativeText(GuiApplication::tr("Some plugins are required for full functionality."));
    message.setDetailedText(GuiApplication::tr("Plugin search locations:\n\n") + Core::pluginPaths().join(u"\n"_s));

    message.addButton(QMessageBox::Ok);
    QPushButton* quitButton = message.addButton(GuiApplication::tr("Quit"), QMessageBox::ActionRole);
    quitButton->setIcon(Gui::iconFromTheme(Constants::Icons::Quit));
    message.setDefaultButton(QMessageBox::Ok);

    message.exec();

    if(message.clickedButton() == quitButton) {
        Application::quit();
    }
}

void GuiApplicationPrivate::initialiseTray()
{
    m_trayIcon = std::make_unique<SystemTrayIcon>(m_actionManager);

    QObject::connect(m_trayIcon.get(), &SystemTrayIcon::toggleVisibility, m_mainWindow.get(),
                     &MainWindow::toggleVisibility);

    if(m_settings->value<Settings::Gui::Internal::ShowTrayIcon>()) {
        m_trayIcon->show();
    }

    m_settings->subscribe<Settings::Gui::Internal::ShowTrayIcon>(m_self,
                                                                 [this](bool show) { m_trayIcon->setVisible(show); });
}

void GuiApplicationPrivate::updateWindowTitle()
{
    if(m_playerController->playState() == Player::PlayState::Stopped) {
        m_mainWindow->resetTitle();
        return;
    }

    const Track currentTrack = m_playerController->currentTrack();
    if(!currentTrack.isValid()) {
        m_mainWindow->resetTitle();
        return;
    }

    auto contextData     = makePlaybackScriptContext(m_playerController, m_playlistHandler->activePlaylist(),
                                                     TrackListContextPolicy::Fallback, {}, false, false,
                                                     Gui::ratingStarSymbols(*m_settings));
    const QString script = m_settings->value<Settings::Gui::Internal::WindowTitleTrackScript>();
    const QString title  = m_scriptParser.evaluate(script, currentTrack, contextData.context);
    m_mainWindow->setTitle(title);
}

void GuiApplicationPrivate::checkArtwork()
{
    if(m_settings->value<Settings::Gui::Internal::ArtworkAutoSearch>()) {
        if(const auto track = m_playerController->currentTrack(); track.isValid()) {
            m_coverProvider.trackHasCover(track).then([this, track](const bool hasCover) {
                if(!hasCover) {
                    m_widgets->showArtworkDialog({track}, Track::Cover::Front, true);
                }
            });
        }
    }
}

void GuiApplicationPrivate::handleTrackStatus(const Engine::TrackStatusContext& context)
{
    const Track& track = context.track;

    if(context.status == Engine::TrackStatus::Invalid) {
        if(track.isValid()) {
            if(track.isInArchive()) {
                if(!QFileInfo::exists(track.archivePath())) {
                    showTrackNotFoundMessage(track);
                }
            }
            else if(!QFileInfo::exists(track.filepath())) {
                showTrackNotFoundMessage(track);
            }
        }
    }
    else if(context.status == Engine::TrackStatus::Unreadable) {
        showTrackUnreableMessage(track);
    }
}

void GuiApplicationPrivate::handleTracksDeleted(const TrackList& tracks)
{
    const PlaylistTrack currentTrack = m_playerController->currentPlaylistTrack();

    const bool deletingCurrentTrack = currentTrack.isValid()
                                   && m_playerController->playState() == Player::PlayState::Playing
                                   && std::ranges::any_of(tracks, [&currentTrack](const Track& track) {
                                          return sameTrackIdentity(track, currentTrack.track);
                                      });

    if(deletingCurrentTrack) {
        // At this point, the tracks have been removed from playlists,
        // so the next track is now the current track (at the same index)
        const PlaylistTrack nextTrack = m_playlistHandler->currentTrack();
        if(nextTrack.isValid()) {
            m_playerController->changeCurrentTrack(
                nextTrack, {.reason = Player::AdvanceReason::ManualNext, .userInitiated = false});
        }
        else {
            m_playerController->stop();
        }
    }
}

void GuiApplicationPrivate::removeExpiredCovers(const TrackList& tracks)
{
    for(const Track& track : tracks) {
        if(track.metadataWasModified()) {
            CoverProvider::removeFromCache(track, *m_settings);
        }
    }
}

void GuiApplicationPrivate::registerActions()
{
    const QStringList volumeCategory = {GuiApplication::tr("Volume")};

    auto* volumeUp = new QAction(GuiApplication::tr("Volume up"), m_mainWindow.get());
    Gui::setThemeIcon(volumeUp, Constants::Icons::VolumeHigh);
    auto* volumeUpCmd = m_actionManager->registerAction(volumeUp, Constants::Actions::VolumeUp);
    volumeUpCmd->setCategories(volumeCategory);
    QObject::connect(volumeUp, &QAction::triggered, m_mainWindow.get(),
                     [this]() { changeVolume(m_settings->value<Settings::Gui::VolumeStep>()); });

    auto* volumeDown = new QAction(GuiApplication::tr("Volume down"), m_mainWindow.get());
    Gui::setThemeIcon(volumeDown, Constants::Icons::VolumeLow);
    auto* volumeDownCmd = m_actionManager->registerAction(volumeDown, Constants::Actions::VolumeDown);
    volumeDownCmd->setCategories(volumeCategory);
    QObject::connect(volumeDown, &QAction::triggered, m_mainWindow.get(),
                     [this]() { changeVolume(-m_settings->value<Settings::Gui::VolumeStep>()); });

    auto* muteAction = new QAction(GuiApplication::tr("Mute"), m_mainWindow.get());
    Gui::setThemeIcon(muteAction, Constants::Icons::VolumeMute);
    auto* muteCmd = m_actionManager->registerAction(muteAction, Constants::Actions::Mute);
    muteCmd->setCategories(volumeCategory);
    QObject::connect(muteAction, &QAction::triggered, m_mainWindow.get(), [this]() { mute(); });

    auto* clearPlaylistAction = new QAction(GuiApplication::tr("Clear Current Playlist"), m_mainWindow.get());
    clearPlaylistAction->setStatusTip(GuiApplication::tr("Remove all tracks from the current playlist"));
    Gui::setThemeIcon(clearPlaylistAction, Constants::Icons::Clear);
    auto* clearPlaylistCmd = m_actionManager->registerAction(clearPlaylistAction, Constants::Actions::ClearPlaylist);
    clearPlaylistCmd->setCategories({GuiApplication::tr("Playlist")});
    clearPlaylistCmd->setAttribute(ProxyAction::UpdateText);
    QObject::connect(clearPlaylistAction, &QAction::triggered, m_mainWindow.get(),
                     [this]() { m_playlistController->clearCurrentPlaylist(); });

    auto updateClearPlaylistState = [this, clearPlaylistAction]() {
        clearPlaylistAction->setEnabled(m_playlistController->canClearCurrentPlaylist());
    };
    QObject::connect(m_playlistController.get(), &PlaylistController::playlistsLoaded, m_mainWindow.get(),
                     updateClearPlaylistState);
    QObject::connect(m_playlistController.get(), &PlaylistController::currentPlaylistChanged, m_mainWindow.get(),
                     updateClearPlaylistState);
    QObject::connect(m_playlistController.get(), &PlaylistController::currentPlaylistUpdated, m_mainWindow.get(),
                     updateClearPlaylistState);
    QObject::connect(m_playlistController.get(), &PlaylistController::currentPlaylistTracksPatched, m_mainWindow.get(),
                     updateClearPlaylistState);
    QObject::connect(m_playlistController.get(), &PlaylistController::currentPlaylistTracksChanged, m_mainWindow.get(),
                     updateClearPlaylistState);
    QObject::connect(m_playlistController.get(), &PlaylistController::currentPlaylistTracksRemoved, m_mainWindow.get(),
                     updateClearPlaylistState);
    updateClearPlaylistState();

    const QStringList seekCategory = {GuiApplication::tr("Playback"), GuiApplication::tr("Seek")};

    auto* seekForwardSmall    = new QAction(GuiApplication::tr("Seek forward (small step)"), m_mainWindow.get());
    auto* seekForwardSmallCmd = m_actionManager->registerAction(seekForwardSmall, Constants::Actions::SeekForwardSmall);
    seekForwardSmallCmd->setCategories(seekCategory);
    seekForwardSmallCmd->setDefaultShortcut(QKeySequence{Qt::ALT | Qt::Key_Right});
    QObject::connect(seekForwardSmall, &QAction::triggered, m_mainWindow.get(),
                     [this]() { m_playerController->seekForward(m_settings->value<Settings::Gui::SeekStepSmall>()); });

    auto* seekForwardLarge    = new QAction(GuiApplication::tr("Seek forward (large step)"), m_mainWindow.get());
    auto* seekForwardLargeCmd = m_actionManager->registerAction(seekForwardLarge, Constants::Actions::SeekForwardLarge);
    seekForwardLargeCmd->setCategories(seekCategory);
    seekForwardLargeCmd->setDefaultShortcut(QKeySequence{Qt::CTRL | Qt::Key_Right});
    QObject::connect(seekForwardLarge, &QAction::triggered, m_mainWindow.get(),
                     [this]() { m_playerController->seekForward(m_settings->value<Settings::Gui::SeekStepLarge>()); });

    auto* seekBackwardSmall = new QAction(GuiApplication::tr("Seek backward (small step)"), m_mainWindow.get());
    auto* seekBackwardSmallCmd
        = m_actionManager->registerAction(seekBackwardSmall, Constants::Actions::SeekBackwardSmall);
    seekBackwardSmallCmd->setCategories(seekCategory);
    seekBackwardSmallCmd->setDefaultShortcut(QKeySequence{Qt::ALT | Qt::Key_Left});
    QObject::connect(seekBackwardSmall, &QAction::triggered, m_mainWindow.get(),
                     [this]() { m_playerController->seekBackward(m_settings->value<Settings::Gui::SeekStepSmall>()); });

    auto* seekBackwardLarge = new QAction(GuiApplication::tr("Seek backward (large step)"), m_mainWindow.get());
    auto* seekBackwardLargeCmd
        = m_actionManager->registerAction(seekBackwardLarge, Constants::Actions::SeekBackwardLarge);
    seekBackwardLargeCmd->setCategories(seekCategory);
    seekBackwardLargeCmd->setDefaultShortcut(QKeySequence{Qt::CTRL | Qt::Key_Left});
    QObject::connect(seekBackwardLarge, &QAction::triggered, m_mainWindow.get(),
                     [this]() { m_playerController->seekBackward(m_settings->value<Settings::Gui::SeekStepLarge>()); });

    auto setSavePlaylistState = [this]() {
        if(auto* savePlaylistCommand = m_actionManager->command(Constants::Actions::SavePlaylist)) {
            if(const auto* playlist = m_playlistController->currentPlaylist()) {
                savePlaylistCommand->action()->setEnabled(playlist->trackCount() > 0);
            }
        }
    };

    QObject::connect(m_playlistController.get(), &PlaylistController::currentPlaylistChanged, m_mainWindow.get(),
                     setSavePlaylistState);
    QObject::connect(m_playlistController->playlistHandler(), &PlaylistHandler::tracksChanged, m_mainWindow.get(),
                     setSavePlaylistState);
    QObject::connect(m_playlistController->playlistHandler(), &PlaylistHandler::tracksAdded, m_mainWindow.get(),
                     setSavePlaylistState);
    QObject::connect(m_playlistController->playlistHandler(), &PlaylistHandler::tracksRemoved, m_mainWindow.get(),
                     setSavePlaylistState);

    auto* removePlaylist = new QAction(GuiApplication::tr("Remove Playlist"), m_mainWindow.get());
    auto* removeCmd      = m_actionManager->registerAction(removePlaylist, Constants::Actions::RemovePlaylist);
    removeCmd->setCategories({GuiApplication::tr("Playlist")});
    removeCmd->setDescription(GuiApplication::tr("Remove Current Playlist"));
    removeCmd->setDefaultShortcut(QKeySequence{Qt::CTRL | Qt::Key_W});
    QObject::connect(removePlaylist, &QAction::triggered, m_mainWindow.get(), [this]() {
        if(auto* currentPlaylist = m_playlistController->currentPlaylist()) {
            m_core->playlistHandler()->removePlaylist(currentPlaylist->id());
        }
    });

    auto* toggleMenubar    = new QAction(GuiApplication::tr("Toggle Menubar"), m_mainWindow.get());
    auto* toggleMenubarCmd = m_actionManager->registerAction(toggleMenubar, Constants::Actions::ToggleMenubar);
    toggleMenubarCmd->setCategories({GuiApplication::tr("View")});
    toggleMenubarCmd->setDefaultShortcut(QKeySequence{Qt::CTRL | Qt::Key_M});
    QObject::connect(toggleMenubar, &QAction::triggered, m_mainWindow.get(), [this]() {
        m_settings->set<Settings::Gui::ShowMenuBar>(!m_settings->value<Settings::Gui::ShowMenuBar>());
    });
}

void GuiApplicationPrivate::rescanTracks(const TrackList& tracks, bool onlyModified) const
{
    auto* scanDialog = new ElapsedProgressDialog(GuiApplication::tr("Reading tracks…"), GuiApplication::tr("Abort"), 0,
                                                 100, Utils::getMainWindow());
    scanDialog->setAttribute(Qt::WA_DeleteOnClose);
    scanDialog->setModal(true);
    scanDialog->setMinimumDuration(500ms);

    auto* library = m_core->library();

    const ScanRequest request = onlyModified ? library->scanModifiedTracks(tracks) : library->scanTracks(tracks);
    scanDialog->startTimer();

    QObject::connect(library, &MusicLibrary::scanProgress, scanDialog,
                     [scanDialog, request](const ScanProgress& progress) {
                         if(progress.id != request.id) {
                             return;
                         }

                         if(scanDialog->wasCancelled()) {
                             request.cancel();
                             scanDialog->close();
                         }

                         const bool isIndeterminate = progress.total <= 0;
                         scanDialog->setBusy(isIndeterminate);

                         if(!isIndeterminate) {
                             scanDialog->setValue(progress.percentage());
                         }
                         if(!progress.file.isEmpty()) {
                             scanDialog->setText(GuiApplication::tr("Current file") + ":\n"_L1 + progress.file);
                         }

                         if(progress.phase == ScanProgress::Phase::Finished) {
                             scanDialog->close();
                         }
                     });
}

void GuiApplicationPrivate::setupScanMenu()
{
    m_selectionController.registerTrackContextSubmenu(m_self, TrackContextMenuArea::Track,
                                                      Constants::Menus::Context::TrackSelection,
                                                      Constants::Menus::Context::Tagging, GuiApplication::tr("Tagging"),
                                                      Constants::Menus::Context::TrackFinalSeparator);

    auto* rescanAction        = new QAction(GuiApplication::tr("Reload tags from files"), m_mainWindow.get());
    auto* rescanChangedAction = new QAction(GuiApplication::tr("Reload tags from modified files"), m_mainWindow.get());

    rescanAction->setStatusTip(GuiApplication::tr("Replace tags in selected tracks with tags from the files"));
    rescanChangedAction->setStatusTip(
        GuiApplication::tr("Replace tags in selected tracks with tags from the files if modified"));

    auto rescan = [this](const bool onlyModified) {
        if(m_selectionController.hasTracks()) {
            const auto tracks = m_selectionController.selectedTracks();
            rescanTracks(tracks, onlyModified);
        }
    };

    QObject::connect(rescanAction, &QAction::triggered, m_mainWindow.get(), [rescan]() { rescan(false); });
    QObject::connect(rescanChangedAction, &QAction::triggered, m_mainWindow.get(), [rescan]() { rescan(true); });

    m_selectionController.registerTrackContextAction(
        m_self, TrackContextMenuArea::Track, Constants::Menus::Context::Tagging, "Tracks.ReloadTags",
        rescanAction->text(), [this, rescanAction](QMenu* menu, const TrackSelection&) {
            rescanAction->setEnabled(m_selectionController.hasTracks());
            menu->addAction(rescanAction);
        });
    m_selectionController.registerTrackContextAction(
        m_self, TrackContextMenuArea::Track, Constants::Menus::Context::Tagging, "Tracks.ReloadModifiedTags",
        rescanChangedAction->text(), [this, rescanChangedAction](QMenu* menu, const TrackSelection&) {
            rescanChangedAction->setEnabled(m_selectionController.hasTracks());
            menu->addAction(rescanChangedAction);
        });
}

void GuiApplicationPrivate::setupRatingMenu()
{
    m_selectionController.registerTrackContextSubmenu(m_self, TrackContextMenuArea::Track,
                                                      Constants::Menus::Context::TrackSelection,
                                                      Constants::Menus::Context::Tagging, GuiApplication::tr("Tagging"),
                                                      Constants::Menus::Context::TrackFinalSeparator);
}

void GuiApplicationPrivate::setupUtilitiesMenu()
{
    m_selectionController.registerTrackContextSubmenu(
        m_self, TrackContextMenuArea::Track, Constants::Menus::Context::TrackSelection,
        Constants::Menus::Context::Utilities, GuiApplication::tr("Utilities"),
        Constants::Menus::Context::TrackFinalSeparator);
}

void GuiApplicationPrivate::close()
{
    if(m_mainWindow->isVisible()) {
        m_mainWindow->close();
    }
    else {
        m_mainWindow->exit();
    }
}

void GuiApplicationPrivate::changeVolume(double delta) const
{
    const double currentVolume = std::max(m_settings->value<Settings::Core::OutputVolume>(), 0.01);
    const double currentDb     = Audio::volumeToDb(currentVolume);
    const double newVolume     = Audio::dbToVolume(currentDb + delta);

    m_settings->set<Settings::Core::OutputVolume>(newVolume);
}

void GuiApplicationPrivate::mute() const
{
    const double volume = m_settings->value<Settings::Core::OutputVolume>();
    if(volume > 0.0) {
        m_settings->set<Settings::Core::Internal::MuteVolume>(volume);
        m_settings->set<Settings::Core::OutputVolume>(0.0);
    }
    else {
        m_settings->set<Settings::Core::OutputVolume>(m_settings->value<Settings::Core::Internal::MuteVolume>());
    }
}

void GuiApplicationPrivate::setStyle() const
{
    const auto currStyle = m_settings->value<Settings::Gui::Style>();
    if(auto* style = QStyleFactory::create(currStyle)) {
        QApplication::setStyle(style);
    }
    else if(auto* systemStyle = QStyleFactory::create(m_settings->value<Settings::Gui::Internal::SystemStyle>())) {
        QApplication::setStyle(systemStyle);
    }
}

void GuiApplicationPrivate::setTheme() const
{
    const auto currTheme = m_settings->value<Settings::Gui::Theme>().value<FyTheme>();

    auto newPalette = m_settings->value<Settings::Gui::Internal::SystemPalette>().value<QPalette>();
    for(const auto& [key, colour] : Utils::asRange(currTheme.colours)) {
        newPalette.setColor(key.group, key.role, colour);
    }
    QApplication::setPalette(newPalette);

    {
        // Reset all fonts to default first
        const auto systemFont = m_settings->value<Settings::Gui::Internal::SystemFont>().value<QFont>();
        QApplication::setFont(systemFont);
        const auto fontEntries = m_themeRegistry->fontEntries();
        for(const auto& [className, _] : fontEntries) {
            if(!className.isEmpty()) {
                QApplication::setFont(systemFont, className.toUtf8().constData());
            }
        }
    }

    auto updateFonts = [currTheme]() {
        for(const auto& [className, font] : Utils::asRange(currTheme.fonts)) {
            if(className.isEmpty()) {
                QApplication::setFont(font);
            }
            else {
                QApplication::setFont(font, className.toUtf8().constData());
            }
        }
        QPixmapCache::clear();
    };
    updateFonts();

    // Fonts can sometimes reset, so set again
    QTimer::singleShot(0, m_self, [updateFonts]() { updateFonts(); });
}

bool GuiApplicationPrivate::setIconTheme() const
{
    const auto [primaryTheme, fallbackTheme] = resolveIconThemes(m_settings);
    return Gui::setThemeIconOverrides(primaryTheme, fallbackTheme);
}

void GuiApplicationPrivate::refreshAutoDetectedIconTheme() const
{
    if(static_cast<IconThemeOption>(m_settings->value<Settings::Gui::IconTheme>()) != IconThemeOption::AutoDetect) {
        return;
    }

    if(!setIconTheme()) {
        return;
    }

    QPixmapCache::clear();
    m_settings->refresh<Settings::Gui::IconTheme>();
}

void GuiApplicationPrivate::registerLayouts()
{
    m_layoutProvider.registerLayout(R"({"Name":"Empty"})");

    m_layoutProvider.registerLayout(
        R"({"Name":"Simple","Widgets":[{"SplitterVertical":{"State":"AAAA/wAAAAEAAAADAAAAHAAAAn0AAAAXAP////8BAAAAAgA=",
            "Widgets":[{"SplitterHorizontal":{"State":"AAAA/wAAAAEAAAAEAAAAggAABqEAAAA+AAAAHAD/////AQAAAAEA","Widgets":[
            {"PlayerControls":{}},{"SeekBar":{}},{"PlaylistControls":{}},{"VolumeControls":{}}]}},{"SplitterHorizontal":{
            "State":"AAAA/wAAAAEAAAACAAAA9QAABAoA/////wEAAAABAA==","Widgets":[{"SplitterVertical":{
            "State":"AAAA/wAAAAEAAAACAAABfQAAAPEA/////wEAAAACAA==","Widgets":[{"LibraryTree":{}},{"ArtworkPanel":{}}]}},
            {"PlaylistTabs":{"Widgets":[{"Playlist":{}}]}}]}},{"StatusBar":{}}]}}]})");

    m_layoutProvider.registerLayout(
        R"({"Name":"Vision","Widgets":[{"SplitterVertical":{"State":"AAAA/wAAAAEAAAADAAAAHAAAA6EAAAAWAP////8BAAAAAgA=",
            "Widgets":[{"SplitterHorizontal":{"State":"AAAA/wAAAAEAAAAEAAAAiQAABk8AAABHAAAAIwD/////AQAAAAEA",
            "Widgets":[{"PlayerControls":{}},{"SeekBar":{}},{"PlaylistControls":{}},{"VolumeControls":{}}]}},
            {"SplitterHorizontal":{"State":"AAAA/wAAAAEAAAACAAADuwAAA48A/////wEAAAABAA==","Widgets":[
            {"TabStack":{"Position":"West","State":"Artwork\u001fInfo\u001fLibrary Tree\u001fPlaylist Organiser",
            "Widgets":[{"ArtworkPanel":{}},{"SelectionInfo":{}},{"LibraryTree":{"Grouping":"Artist/Album"}},
            {"PlaylistOrganiser":{}}]}},{"Playlist":{}}]}},{"StatusBar":{}}]}}]})");

    m_layoutProvider.registerLayout(
        R"({"Name":"Browser","Widgets":[{"SplitterVertical":{"State":"AAAA/wAAAAEAAAADAAAAFwAAA6YAAAAWAP////8BAAAAAgA=",
            "Widgets":[{"SplitterHorizontal":{"State":"AAAA/wAAAAEAAAAEAAAAcgAABRoAAAA2AAAAGAD/////AQAAAAEA",
            "Widgets":[{"PlayerControls":{}},{"SeekBar":{}},{"PlaylistControls":{}},{"VolumeControls":{}}]}},
            {"SplitterHorizontal":{"State":"AAAA/wAAAAEAAAACAAACeAAAAnoA/////wEAAAABAA==x","Widgets":[{"DirectoryBrowser":{}},
            {"ArtworkPanel":{}}]}},{"StatusBar":{}}]}}]})");
}

void GuiApplicationPrivate::checkTracksNeedUpdate() const
{
    const auto libraryOutOfDate = [this](int threshold) -> bool {
        const int prev = m_core->database()->previousRevision();
        const int curr = m_core->database()->currentRevision();
        return prev > 0 && prev < threshold && curr >= threshold;
    };

    if(m_library->hasLibrary()) {
        if(libraryOutOfDate(7)     /* changed codec storage type */
           || libraryOutOfDate(11) /* removed ReplayGain data in extra tags */
           || libraryOutOfDate(13) /* added more tech fields */
           || libraryOutOfDate(17) /* added file creation date */) {
            showNeedReloadMessage();
        }
    }
}

void GuiApplicationPrivate::showNeedReloadMessage() const
{
    QMessageBox message;
    message.setIcon(QMessageBox::Warning);
    message.setText(GuiApplication::tr("Reload Required"));
    message.setInformativeText(GuiApplication::tr(
        "Due to a database change, tracks should be reloaded from disk to update their saved metadata."));

    message.addButton(QMessageBox::Ok);
    if(auto* button = message.button(QMessageBox::Ok)) {
        button->setText(GuiApplication::tr("Reload Now"));
    }
    QPushButton* okButton = message.addButton(GuiApplication::tr("OK"), QMessageBox::ActionRole);
    message.setDefaultButton(QMessageBox::Ok);

    message.exec();

    if(message.clickedButton() != okButton) {
        m_library->rescanAll();
    }
}

void GuiApplicationPrivate::showScriptEditor()
{
    auto* scriptEditor
        = new ScriptEditor(m_core->libraryManager(), &m_selectionController, m_playerController, m_mainWindow.get());
    scriptEditor->setAttribute(Qt::WA_DeleteOnClose);
    scriptEditor->show();
}

void GuiApplicationPrivate::showSearchPlaylistDialog()
{
    auto* coverProvider = new CoverProvider(m_core->audioLoader(), m_settings, m_self);
    auto* search        = new SearchDialog(m_actionManager, &m_playlistInteractor, coverProvider, m_core,
                                           SearchDialog::Target::Playlist);
    search->setAttribute(Qt::WA_DeleteOnClose);
    coverProvider->setParent(search);

    search->show();
}

void GuiApplicationPrivate::showSearchLibraryDialog()
{
    auto* coverProvider = new CoverProvider(m_core->audioLoader(), m_settings, m_self);
    auto* search        = new SearchDialog(m_actionManager, &m_playlistInteractor, coverProvider, m_core,
                                           SearchDialog::Target::Library);
    search->setAttribute(Qt::WA_DeleteOnClose);
    coverProvider->setParent(search);

    search->show();
}

void GuiApplicationPrivate::showPlaylistManager()
{
    if(m_playlistManagerWidget) {
        m_playlistManagerWidget->show();
        m_playlistManagerWidget->raise();
        m_playlistManagerWidget->activateWindow();
        return;
    }

    m_playlistManagerWidget
        = new PlaylistManagerWidget(m_actionManager, m_playlistController.get(), &m_playlistInteractor, m_settings);
    m_playlistManagerWidget->setAttribute(Qt::WA_DeleteOnClose);
    m_playlistManagerWidget->finalise();

    m_playlistManagerWidget->show();
}

void GuiApplicationPrivate::showPlaybackQueue()
{
    if(m_playbackQueueWidget) {
        m_playbackQueueWidget->show();
        m_playbackQueueWidget->raise();
        m_playbackQueueWidget->activateWindow();
        return;
    }

    m_playbackQueueWidget = new QueueViewer(m_actionManager, &m_playlistInteractor, m_core->audioLoader(), m_settings);
    m_playbackQueueWidget->setAttribute(Qt::WA_DeleteOnClose);
    m_playbackQueueWidget->finalise();

    m_playbackQueueWidget->show();
}

void GuiApplicationPrivate::focusSearchBar() const
{
    const auto searchBars = m_editableLayout->findWidgetsByType<SearchWidget>();
    for(auto* searchBar : searchBars) {
        if(searchBar && searchBar->isVisibleTo(m_mainWindow.get())) {
            searchBar->setFocus(Qt::ShortcutFocusReason);
            return;
        }
    }
}

void GuiApplicationPrivate::showQuickSearch() const
{
    auto* searchBar = new SearchWidget(m_searchController, m_playlistController.get(), m_library, m_settings);
    searchBar->setAttribute(Qt::WA_DeleteOnClose);
    searchBar->setWindowTitle(SearchWidget::tr("Quick Search"));
    searchBar->show();
}

void GuiApplicationPrivate::showPropertiesDialog(const TrackList& tracks) const
{
    if(!tracks.empty()) {
        m_propertiesDialog->show(tracks);
    }
}

void GuiApplicationPrivate::showEngineError(const QString& error) const
{
    if(error.isEmpty()) {
        return;
    }

    QMessageBox message;
    message.setIcon(QMessageBox::Warning);
    message.setText(GuiApplication::tr("Playback Error"));
    message.setInformativeText(error);

    message.addButton(QMessageBox::Ok);

    message.exec();
}

void GuiApplicationPrivate::showMessage(const QString& title, const Track& track) const
{
    if(m_settings->fileValue(Settings::Core::Internal::PlaylistSkipUnavailable).toBool()) {
        m_playerController->next();
        if(m_playerController->playState() == Player::PlayState::Playing) {
            m_playerController->play();
        }
        return;
    }

    QMessageBox message;
    message.setIcon(QMessageBox::Warning);
    message.setText(title);
    message.setInformativeText(track.filepath());

    message.addButton(QMessageBox::Ok);
    if(auto* button = message.button(QMessageBox::Ok)) {
        button->setText(GuiApplication::tr("Continue"));
    }
    QPushButton* stopButton = message.addButton(GuiApplication::tr("Stop"), QMessageBox::ActionRole);
    stopButton->setIcon(Gui::iconFromTheme(Constants::Icons::Stop));
    message.setDefaultButton(QMessageBox::Ok);

    auto* alwaysSkip = new QCheckBox(GuiApplication::tr("Always continue playing if a track is unavailable"), &message);
    message.setCheckBox(alwaysSkip);

    message.exec();

    if(alwaysSkip->isChecked()) {
        m_settings->fileSet(Settings::Core::Internal::PlaylistSkipUnavailable, true);
    }

    if(message.clickedButton() == stopButton) {
        m_playerController->stop();
    }
    else {
        m_playerController->next();
        if(m_playerController->playState() == Player::PlayState::Playing) {
            m_playerController->play();
        }
    }
}

void GuiApplicationPrivate::showTrackNotFoundMessage(const Track& track) const
{
    showMessage(GuiApplication::tr("Track Not Found"), track);
}

void GuiApplicationPrivate::showTrackUnreableMessage(const Track& track) const
{
    showMessage(GuiApplication::tr("No Decoder Available"), track);
}

void GuiApplicationPrivate::createNewPlaylist() const
{
    if(auto* playlist = m_playlistHandler->createEmptyPlaylist()) {
        m_playlistController->changeCurrentPlaylist(playlist);
    }
}

void GuiApplicationPrivate::createNewAutoPlaylist()
{
    auto* autoDialog = new AutoPlaylistDialog(m_mainWindow.get());
    autoDialog->setAttribute(Qt::WA_DeleteOnClose);
    QObject::connect(autoDialog, &AutoPlaylistDialog::playlistEdited, autoDialog,
                     [this](const QString& name, const QString& query, const QString& sortQuery, bool forceSorted) {
                         if(auto* playlist
                            = m_playlistHandler->createNewAutoPlaylist(name, query, sortQuery, forceSorted)) {
                             m_playlistController->changeCurrentPlaylist(playlist);
                         }
                     });
    autoDialog->show();
}

void GuiApplicationPrivate::addFiles()
{
    const QString audioExtensions
        = Utils::extensionsToWildcards(m_core->audioLoader()->supportedFileExtensions()).join(" "_L1);
    const QString allExtensions = u"%1 %2"_s.arg(audioExtensions, u"*.cue"_s);

    const QString allFilter   = GuiApplication::tr("All Supported Media Files (%1)").arg(allExtensions);
    const QString filesFilter = GuiApplication::tr("Audio Files (%1)").arg(audioExtensions);
    const QStringList filters{allFilter, filesFilter};

    FyStateSettings stateSettings;

    QUrl dir = QUrl::fromLocalFile(QDir::homePath());
    if(const auto lastPath = stateSettings.value(Settings::Gui::Internal::LastFilePath).toString();
       !lastPath.isEmpty()) {
        dir = lastPath;
    }

    const auto files = QFileDialog::getOpenFileUrls(m_mainWindow.get(), GuiApplication::tr("Add Files"), dir,
                                                    filters.join(";;"_L1), nullptr, QFileDialog::DontResolveSymlinks);

    if(files.empty()) {
        return;
    }

    stateSettings.setValue(Settings::Gui::Internal::LastFilePath, files.front());

    m_playlistInteractor.filesToCurrentPlaylist(files);
}

void GuiApplicationPrivate::addFolders()
{
    const auto dirs = QFileDialog::getExistingDirectoryUrl(m_mainWindow.get(), GuiApplication::tr("Add Folders"),
                                                           QDir::homePath(), QFileDialog::DontResolveSymlinks);

    if(dirs.isEmpty()) {
        return;
    }

    m_playlistInteractor.filesToCurrentPlaylist({dirs});
}

void GuiApplicationPrivate::openFiles(const QList<QUrl>& urls)
{
    QList<QUrl> files{urls};
    QUrl fileToPlay;

    if(m_settings->value<Settings::Core::OpenFileAddDirectory>() && urls.size() == 1 && urls.front().isLocalFile()) {
        const QFileInfo fileInfo{urls.front().toLocalFile()};
        if(fileInfo.isFile()) {
            QStringList supportedExtensions
                = Utils::extensionsToWildcards(m_core->audioLoader()->supportedFileExtensions());
            supportedExtensions.append(u"*.cue"_s);

            const QList<QUrl> dirFiles = Utils::File::getUrlsInDir(QDir{fileInfo.absolutePath()}, supportedExtensions);
            if(!dirFiles.empty()) {
                files      = dirFiles;
                fileToPlay = urls.front();
            }
        }
    }

    const QString playlistName = m_settings->value<Settings::Core::OpenFilesPlaylist>();
    const bool replacePlaylist = m_settings->value<Settings::Core::OpenFilesSendTo>();
    if(!fileToPlay.isEmpty()) {
        m_playlistInteractor.filesToNewPlaylist(playlistName, files, fileToPlay, replacePlaylist, true);
    }
    else if(replacePlaylist) {
        m_playlistInteractor.filesToNewPlaylistReplace(playlistName, files, true);
    }
    else {
        m_playlistInteractor.filesToNewPlaylist(playlistName, files, true);
    }
}

void GuiApplicationPrivate::loadPlaylist()
{
    const QString allExtensions
        = Utils::extensionsToWildcards(m_core->playlistLoader()->supportedExtensions()).join(" "_L1);
    const auto playlistFilter
        = Utils::extensionsToFilterList(m_core->playlistLoader()->supportedExtensions(), u"playlists"_s);

    const QString allFilter = GuiApplication::tr("All Supported Playlists (%1)").arg(allExtensions);

    const QStringList filters{allFilter, playlistFilter};

    QUrl dir = QUrl::fromLocalFile(QDir::homePath());
    if(const auto lastPath = m_settings->fileValue(Settings::Gui::Internal::LastFilePath).toString();
       !lastPath.isEmpty()) {
        dir = lastPath;
    }

    const auto files = QFileDialog::getOpenFileUrls(m_mainWindow.get(), GuiApplication::tr("Load Playlist"), dir,
                                                    filters.join(";;"_L1), nullptr, QFileDialog::DontResolveSymlinks);

    if(files.empty()) {
        return;
    }

    m_settings->fileSet(Settings::Gui::Internal::LastFilePath, files.front());
    const QList<QPair<QString, QUrl>> info{[&files]() {
        QList<QPair<QString, QUrl>> list;
        for(const QUrl& url : files) {
            list.append(qMakePair(QFileInfo(url.toLocalFile()).completeBaseName(), url));
        }
        return list;
    }()};

    m_playlistInteractor.loadPlaylist(info);
}

void GuiApplicationPrivate::savePlaylist() const
{
    auto* playlist = m_playlistController->currentPlaylist();
    savePlaylist(playlist);
}

void GuiApplicationPrivate::savePlaylist(const Playlist* playlist) const
{
    if(!playlist || playlist->trackCount() == 0) {
        return;
    }

    const QString playlistFilter
        = Utils::extensionsToFilterList(m_core->playlistLoader()->supportedSaveExtensions(), u"files"_s);

    QDir dir{QDir::homePath()};
    if(const auto lastPath = m_settings->fileValue(Settings::Gui::Internal::LastFilePath).toString();
       !lastPath.isEmpty()) {
        dir = lastPath;
    }

    QFileDialog saveDialog{m_mainWindow.get(), GuiApplication::tr("Save Playlist"), dir.absolutePath(), playlistFilter};
    saveDialog.setAcceptMode(QFileDialog::AcceptSave);
    saveDialog.setFileMode(QFileDialog::AnyFile);
    saveDialog.setOption(QFileDialog::DontResolveSymlinks);

    const QStringList filters = playlistFilter.split(";;"_L1, Qt::SkipEmptyParts);
    if(!filters.isEmpty()) {
        saveDialog.selectNameFilter(filters.constFirst());
        if(const QString defaultSuffix = Utils::extensionFromFilter(filters.constFirst()); !defaultSuffix.isEmpty()) {
            saveDialog.setDefaultSuffix(defaultSuffix);
            saveDialog.selectFile(playlist->name() + u'.' + defaultSuffix);
        }
        QObject::connect(&saveDialog, &QFileDialog::filterSelected, &saveDialog, [&saveDialog](const QString& filter) {
            if(const QString suffix = Utils::extensionFromFilter(filter); !suffix.isEmpty()) {
                saveDialog.setDefaultSuffix(suffix);
            }
        });
    }
    else {
        saveDialog.selectFile(playlist->name());
    }

    if(saveDialog.exec() == 0) {
        return;
    }

    const QStringList selectedFiles = saveDialog.selectedFiles();
    if(selectedFiles.isEmpty()) {
        return;
    }

    const QString& file = selectedFiles.constFirst();
    if(file.isEmpty()) {
        return;
    }

    FyStateSettings stateSettings;
    stateSettings.setValue(Settings::Gui::Internal::LastFilePath, file);

    const QString extension = Utils::extensionFromFilter(saveDialog.selectedNameFilter());
    if(extension.isEmpty()) {
        return;
    }

    const QFileInfo info{file};
    if(auto* parser = m_core->playlistLoader()->parserForExtension(extension)) {
        QFile playlistFile{file};
        if(!playlistFile.open(QIODevice::WriteOnly)) {
            qCWarning(GUI_APP) << "Could not open playlist file" << file
                               << "for writing:" << playlistFile.errorString();
            return;
        }

        const QDir playlistDir{info.absolutePath()};
        const auto pathType = static_cast<PlaylistParser::PathType>(
            m_settings->fileValue(Settings::Core::Internal::PlaylistSavePathType, 0).toInt());
        const bool writeMetadata
            = m_settings->fileValue(Settings::Core::Internal::PlaylistSaveMetadata, false).toBool();

        parser->savePlaylist(&playlistFile, extension, playlist->tracks(), playlistDir, pathType, writeMetadata);
    }
}

void GuiApplication::savePlaylist(const UId& playlistId) const
{
    p->savePlaylist(p->m_playlistHandler->playlistById(playlistId));
}

void GuiApplicationPrivate::saveAllPlaylist() const
{
    auto* savePlaylists = new SavePlaylistsDialog(m_core, m_mainWindow.get());
    savePlaylists->setAttribute(Qt::WA_DeleteOnClose);
    savePlaylists->show();
}

GuiApplication::GuiApplication(Application* core)
    : p{createGuiApplicationPrivate(this, core)}
{
    QObject::connect(p->m_settings->settingsDialog(), &SettingsDialogController::opening, this, [this]() {
        const bool isLayoutEditing = p->m_settings->value<Settings::Gui::LayoutEditing>();
        // Layout editing mode overrides the global action context, so disable it until the dialog closes
        p->m_settings->set<Settings::Gui::LayoutEditing>(false);
        QObject::connect(
            p->m_settings->settingsDialog(), &SettingsDialogController::closing, this,
            [this, isLayoutEditing]() { p->m_settings->set<Settings::Gui::LayoutEditing>(isLayoutEditing); },
            Qt::SingleShotConnection);
    });
}

bool GuiApplication::eventFilter(QObject* watched, QEvent* event)
{
    if(watched == qApp) {
        switch(event->type()) {
            case QEvent::ApplicationPaletteChange:
            case QEvent::ThemeChange:
                p->refreshAutoDetectedIconTheme();
                break;
            default:
                break;
        }
    }
    else if(watched == p->m_mainWindow.get()) {
        switch(event->type()) {
            case QEvent::PaletteChange:
            case QEvent::StyleChange:
            case QEvent::ThemeChange:
                p->refreshAutoDetectedIconTheme();
                break;
            default:
                break;
        }
    }

    return QObject::eventFilter(watched, event);
}

void GuiApplication::startup()
{
    p->initialise();
}

GuiApplication::~GuiApplication() = default;

void GuiApplication::shutdown()
{
    qApp->removeEventFilter(this);
    p->m_actionManager->saveSettings();
    p->m_editableLayout->saveLayout();
    p->m_editableLayout.reset();
    p->m_playlistController.reset();
    p->m_mainWindow.reset();
}

void GuiApplication::raise()
{
    p->m_mainWindow->raiseWindow();
}

void GuiApplication::openFiles(const QList<QUrl>& files)
{
    if(p->m_playlistController->playlistsHaveLoaded()) {
        p->openFiles(files);
        return;
    }

    QObject::connect(
        p->m_playlistController.get(), &PlaylistController::playlistsLoaded, this,
        [this, files]() { p->openFiles(files); }, Qt::SingleShotConnection);
}

void GuiApplication::searchForArtwork(const TrackList& tracks, Track::Cover type, bool quick)
{
    if(!quick) {
        auto* search = new ArtworkDialog(p->m_core->networkManager(), p->m_core->library(), p->m_settings, tracks, type,
                                         p->m_mainWindow.get());
        search->setAttribute(Qt::WA_DeleteOnClose);
        search->show();
        return;
    }

    StatusEvent::post(GuiApplication::tr("Searching for artwork…"), 0);

    auto* artworkFinder = new ArtworkFinder(p->m_core->networkManager(), p->m_settings, this);

    const Track& track = tracks.front();
    ScriptParser parser;

    const QString artist = parser.evaluate(p->m_settings->value<Settings::Gui::Internal::ArtworkArtistField>(), track);
    const QString album  = parser.evaluate(p->m_settings->value<Settings::Gui::Internal::ArtworkAlbumField>(), track);
    const QString title{u"%title%"_s};

    const auto finishSearch = [artworkFinder]() {
        artworkFinder->disconnect();
        artworkFinder->deleteLater();
        StatusEvent::post(GuiApplication::tr("Artwork search finished"));
    };

    QObject::connect(
        artworkFinder, &ArtworkFinder::coverLoaded, this,
        [this, finishSearch, tracks](const QUrl& /*url*/, const Fooyin::ArtworkResult& result) {
            finishSearch();

            const auto saveMethods
                = p->m_settings->value<Settings::Gui::Internal::ArtworkSaveMethods>().value<ArtworkSaveMethods>();
            const auto& frontMethod = saveMethods[Track::Cover::Front];
            const auto saveResult   = prepareArtworkForSave(result, frontMethod.format, frontMethod.quality);

            if(frontMethod.method == ArtworkSaveMethod::Embedded) {
                TrackCoverData coverData;
                coverData.tracks = tracks;
                coverData.coverData.emplace(Track::Cover::Front,
                                            CoverImage{.mimeType = saveResult.mimeType, .data = saveResult.image});
                p->m_library->writeTrackCovers(coverData).finished.then(
                    this, [this, tracks](const WriteResult& /*result*/) {
                        for(const Track& updatedTrack : tracks) {
                            CoverProvider::removeFromCache(updatedTrack, *p->m_settings);
                        }
                    });
            }
            else {
                ScriptParser trackParser;
                const QString path = trackParser.evaluate(
                    u"%1/%2.%3"_s.arg(frontMethod.dir, frontMethod.filename, saveResult.suffix), tracks.front());
                const QString cleanPath = QDir::cleanPath(path);

                QFile file{cleanPath};
                if(file.open(QIODevice::WriteOnly) && file.write(saveResult.image) == saveResult.image.size()) {
                    for(const Track& updatedTrack : tracks) {
                        CoverProvider::removeFromCache(updatedTrack, *p->m_settings);
                    }
                    p->m_widgets->refreshCoverWidgets();
                }
            }
        });

    QObject::connect(artworkFinder, &ArtworkFinder::searchFinished, this, [finishSearch]() { finishSearch(); });

    artworkFinder->findArtwork(Track::Cover::Front, artist, album, title);
}

void GuiApplication::removeArtwork(const TrackList& tracks, const std::set<Track::Cover>& types)
{
    TrackCoverData coverData;
    coverData.tracks = tracks;

    for(const auto& coverType : types) {
        coverData.coverData.emplace(coverType, CoverImage{});
    }

    p->m_library->writeTrackCovers(coverData).finished.then(this, [this, tracks](const WriteResult& /*result*/) {
        for(const Track& track : tracks) {
            CoverProvider::removeFromCache(track, *p->m_settings);
        }
    });
}

void GuiApplication::attachArtwork(const TrackList& tracks, Track::Cover type, const QString& filepath)
{
    if(tracks.empty() || filepath.isEmpty()) {
        return;
    }

    QFile file{filepath};
    if(!file.open(QIODevice::ReadOnly)) {
        StatusEvent::post(tr("Failed to open artwork file"));
        return;
    }

    const QByteArray image = file.readAll();
    if(image.isEmpty()) {
        StatusEvent::post(tr("Artwork file is empty"));
        return;
    }

    const QString mimeType = QMimeDatabase().mimeTypeForData(image).name();

    TrackCoverData coverData;
    coverData.tracks = tracks;
    coverData.coverData.emplace(type, CoverImage{.mimeType = mimeType, .data = image});

    p->m_library->writeTrackCovers(coverData).finished.then(this, [this, tracks](const WriteResult& /*result*/) {
        for(const Track& track : tracks) {
            CoverProvider::removeFromCache(track, *p->m_settings);
        }
    });
}

void GuiApplication::removeAllArtwork(const TrackList& tracks)
{
    removeArtwork(tracks, {Track::Cover::Artist, Track::Cover::Back, Track::Cover::Front, Track::Cover::Other});
}

ActionManager* GuiApplication::actionManager() const
{
    return p->m_actionManager;
}

LayoutProvider* GuiApplication::layoutProvider() const
{
    return &p->m_layoutProvider;
}

PlaylistController* GuiApplication::playlistController() const
{
    return p->m_playlistController.get();
}

TrackSelectionController* GuiApplication::trackSelection() const
{
    return &p->m_selectionController;
}

SearchController* GuiApplication::searchController() const
{
    return p->m_searchController;
}

PropertiesDialog* GuiApplication::propertiesDialog() const
{
    return p->m_propertiesDialog;
}

ScriptCommandHandler* GuiApplication::scriptCommandHandler() const
{
    return p->m_scriptCommandHandler.get();
}

EditableLayout* GuiApplication::editableLayout() const
{
    return p->m_editableLayout.get();
}

WidgetProvider* GuiApplication::widgetProvider() const
{
    return &p->m_widgetProvider;
}

ThemeRegistry* GuiApplication::themeRegistry() const
{
    return p->m_themeRegistry;
}

AdvancedSettingsRegistry* GuiApplication::advancedSettingsRegistry() const
{
    return p->m_advancedSettingsRegistry.get();
}
} // namespace Fooyin

#include "moc_guiapplication.cpp"
