/*
 * Fooyin
 * Copyright © 2023, Luke Taylor <luket@pm.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 "librarytreewidget.h"

#include "contextmenuids.h"
#include "internalguisettings.h"
#include "librarytree/librarytreecontroller.h"
#include "librarytreeconfigwidget.h"
#include "librarytreedelegate.h"
#include "librarytreegroupeditordialog.h"
#include "librarytreegroupregistry.h"
#include "librarytreemodel.h"
#include "librarytreeview.h"
#include "playlist/playlistcontroller.h"

#include <core/application.h>
#include <core/coresettings.h>
#include <core/library/libraryinfo.h>
#include <core/library/musiclibrary.h>
#include <core/player/playercontroller.h>
#include <core/player/playerdefs.h>
#include <core/playlist/playlisthandler.h>
#include <core/scripting/trackqueryfilter.h>
#include <gui/configdialog.h>
#include <gui/contextmenuutils.h>
#include <gui/guiconstants.h>
#include <gui/guisettings.h>
#include <gui/guistyleprovider.h>
#include <gui/guiutils.h>
#include <gui/iconloader.h>
#include <gui/trackselectioncontroller.h>
#include <utils/actions/actionmanager.h>
#include <utils/actions/command.h>
#include <utils/actions/widgetcontext.h>
#include <utils/async.h>
#include <utils/datastream.h>
#include <utils/fileutils.h>
#include <utils/signalthrottler.h>
#include <utils/tooltipfilter.h>
#include <utils/utils.h>

#include <QActionGroup>
#include <QComboBox>
#include <QDialog>
#include <QFileInfo>
#include <QGridLayout>
#include <QHeaderView>
#include <QJsonObject>
#include <QKeyEvent>
#include <QLineEdit>
#include <QMenu>
#include <QTreeView>
#include <QVBoxLayout>

#include <algorithm>
#include <stack>

using namespace Qt::StringLiterals;

constexpr auto LibTreePlaylist = "␟LibTreePlaylist␟";

// Settings
constexpr auto LibTreeDoubleClickKey           = u"LibraryTree/DoubleClickBehaviour";
constexpr auto LibTreeMiddleClickKey           = u"LibraryTree/MiddleClickBehaviour";
constexpr auto LibTreePlaylistEnabledKey       = u"LibraryTree/SelectionPlaylistEnabled";
constexpr auto LibTreeAutoSwitchKey            = u"LibraryTree/SelectionPlaylistAutoSwitch";
constexpr auto LibTreeAutoPlaylistKey          = u"LibraryTree/SelectionPlaylistName";
constexpr auto LibTreeScrollBarKey             = u"LibraryTree/Scrollbar";
constexpr auto LibTreeAltColoursKey            = u"LibraryTree/AlternatingColours";
constexpr auto LibTreeRowHeightKey             = u"LibraryTree/RowHeight";
constexpr auto LibTreeSendPlaybackKey          = u"LibraryTree/StartPlaybackOnSend";
constexpr auto LibTreeRestoreStateKey          = u"LibraryTree/RestoreState";
constexpr auto LibTreeExpandSingleClickKey     = u"LibraryTree/ExpandOnSingleClick";
constexpr auto LibTreeAutoExpandSearchLimitKey = u"LibraryTree/AutoExpandSearchResultLimit";
constexpr auto LibTreeKeepAliveKey             = u"LibraryTree/KeepAlive";
constexpr auto LibTreeAnimatedKey              = u"LibraryTree/Animated";
constexpr auto LibTreeHeaderKey                = u"LibraryTree/Header";
constexpr auto LibTreeShowSummaryNodeKey       = u"LibraryTree/ShowSummaryNode";
constexpr auto LibTreeSummaryNodeTitleKey      = u"LibraryTree/SummaryNodeTitle";

namespace Fooyin {
using namespace Settings::Gui::Internal;

namespace {
QModelIndexList filterAncestors(const QModelIndexList& indexes)
{
    QModelIndexList filteredIndexes;
    QModelIndexList indexesToRemove;

    for(const QModelIndex& index : indexes) {
        const QModelIndex parentIndex = index.parent();
        if(indexes.contains(parentIndex) || filteredIndexes.contains(parentIndex)) {
            for(const QModelIndex& childIndex : filteredIndexes) {
                if(childIndex.parent() == parentIndex) {
                    indexesToRemove.emplace_back(childIndex);
                }
            }
        }
        else {
            filteredIndexes.emplace_back(index);
        }
    }

    for(const QModelIndex& index : std::as_const(indexesToRemove)) {
        filteredIndexes.removeAll(index);
    }

    return filteredIndexes;
}

QModelIndexList getAllChildren(QAbstractItemModel* model, const QModelIndex& parent)
{
    QModelIndexList children;

    if(model->canFetchMore(parent)) {
        model->fetchMore(parent);
    }

    const int childCount = model->rowCount(parent);
    for(int row{0}; row < childCount; ++row) {
        const QModelIndex child = model->index(row, 0, parent);
        children.append(child);
        children.append(getAllChildren(model, child));
    }

    return children;
}

QModelIndexList filterLeafNodes(QAbstractItemModel* model, const QModelIndexList& indexes)
{
    QModelIndexList leafNodes;
    QSet<QModelIndex> processedIndexes;

    for(const QModelIndex& index : indexes) {
        const QModelIndexList children = getAllChildren(model, index);

        if(children.isEmpty()) {
            leafNodes.append(index);
        }
        else {
            for(const QModelIndex& child : children) {
                if(model->rowCount(child) == 0 && !processedIndexes.contains(child)) {
                    leafNodes.append(child);
                    processedIndexes.insert(child);
                }
            }
        }
    }

    return leafNodes;
}

TrackList tracksForIndexes(LibraryTreeSortModel* model, const QModelIndexList& indexes)
{
    TrackList tracks;
    int totalTrackCount{0};

    for(const QModelIndex& index : indexes) {
        totalTrackCount += index.data(LibraryTreeItem::TrackCount).toInt();
    }

    if(totalTrackCount > 0) {
        tracks.reserve(static_cast<size_t>(totalTrackCount));
    }

    model->appendTracksForIndexes(indexes, tracks);

    return tracks;
}

} // namespace

LibraryTreeWidget::LibraryTreeWidget(ActionManager* actionManager, PlaylistController* playlistController,
                                     TrackSelectionController* trackSelection, LibraryTreeController* controller,
                                     Application* core, CoverRepository* coverRepository,
                                     GuiStyleProvider* styleProvider, QWidget* parent)
    : FyWidget{parent}
    , m_actionManager{actionManager}
    , m_library{core->library()}
    , m_playlistHandler{playlistController->playlistHandler()}
    , m_playerController{playlistController->playerController()}
    , m_playlistController{playlistController}
    , m_groupsRegistry{controller->groupRegistry()}
    , m_trackSelection{trackSelection}
    , m_settings{core->settingsManager()}
    , m_styleProvider{styleProvider}
    , m_resetThrottler{new SignalThrottler(this)}
    , m_layout{new QVBoxLayout(this)}
    , m_libraryTree{new LibraryTreeView(this)}
    , m_model{new LibraryTreeModel(core->libraryManager(), core->audioLoader(), coverRepository, m_settings,
                                   styleProvider, this)}
    , m_sortProxy{new LibraryTreeSortModel(this)}
    , m_widgetContext{new WidgetContext(
          this,
          Context{IdList{Constants::Context::TrackSelection, Id{"Fooyin.Context.LibraryTree."}.append(this->id())}},
          this)}
    , m_addToQueueAction{new QAction(tr("&Add to playback queue"), this)}
    , m_queueNextAction{new QAction(tr("&Queue to play next"), this)}
    , m_removeFromQueueAction{new QAction(tr("&Remove from playback queue"), this)}
    , m_playAction{new QAction(tr("&Play"), this)}
    , m_doubleClickAction{TrackAction::None}
    , m_middleClickAction{TrackAction::None}
    , m_currentEmptySearchMode{EmptySearchMode::Clear}
    , m_updating{false}
    , m_playlist{nullptr}
{
    m_layout->setContentsMargins(0, 0, 0, 0);
    m_layout->addWidget(m_libraryTree);

    m_sortProxy->setSourceModel(m_model);
    m_libraryTree->setModel(m_sortProxy);
    m_libraryTree->setItemDelegate(new LibraryTreeDelegate(this));
    m_libraryTree->viewport()->installEventFilter(new ToolTipFilter(this));
    QObject::connect(m_libraryTree, &LibraryTreeView::displayChanged, m_model, &LibraryTreeModel::resetPalette);

    if(const auto initialGroup = m_groupsRegistry->itemByIndex(0)) {
        changeGrouping(initialGroup.value());
    }

    m_actionManager->addContextObject(m_widgetContext);
    Context actionContext = m_widgetContext->context();
    actionContext.erase(Constants::Context::TrackSelection);

    m_model->setPlayState(playlistController->playerController()->playState());

    Gui::setThemeIcon(m_addToQueueAction, Constants::Icons::Add);
    Gui::setThemeIcon(m_removeFromQueueAction, Constants::Icons::Remove);
    Gui::setThemeIcon(m_playAction, Constants::Icons::Play);

    m_playAction->setStatusTip(tr("Start playback of the selected tracks"));
    QObject::connect(m_playAction, &QAction::triggered, this,
                     [this]() { handlePlayback(m_libraryTree->selectionModel()->selectedRows()); });

    m_addToQueueAction->setEnabled(false);
    m_actionManager->registerAction(m_addToQueueAction, Constants::Actions::AddToQueue, actionContext);
    QObject::connect(m_addToQueueAction, &QAction::triggered, this, [this]() { queueSelectedTracks(false); });

    m_queueNextAction->setEnabled(false);
    m_actionManager->registerAction(m_queueNextAction, Constants::Actions::QueueNext, actionContext);
    QObject::connect(m_queueNextAction, &QAction::triggered, this, [this]() { queueSelectedTracks(true); });

    m_removeFromQueueAction->setVisible(false);
    m_actionManager->registerAction(m_removeFromQueueAction, Constants::Actions::RemoveFromQueue, actionContext);
    QObject::connect(m_removeFromQueueAction, &QAction::triggered, this, [this]() { dequeueSelectedTracks(); });

    setObjectName(LibraryTreeWidget::name());
    setFeature(FyWidget::Search);

    m_config = defaultConfig();
    applyConfig(m_config);

    setupConnections();
}

LibraryTreeWidget::~LibraryTreeWidget()
{
    QObject::disconnect(m_resetThrottler, nullptr, this, nullptr);
}

QString LibraryTreeWidget::name() const
{
    return tr("Library Tree");
}

QString LibraryTreeWidget::layoutName() const
{
    return u"LibraryTree"_s;
}

void LibraryTreeWidget::saveLayoutData(QJsonObject& layout)
{
    saveConfigToLayout(m_config, layout);
    layout["Grouping"_L1] = m_grouping.id;
    layout["State"_L1]    = QString::fromUtf8(saveState().toBase64());
}

void LibraryTreeWidget::loadLayoutData(const QJsonObject& layout)
{
    applyConfig(configFromLayout(layout));

    if(layout.contains("Grouping"_L1)) {
        if(const auto grouping = m_groupsRegistry->itemById(layout.value("Grouping"_L1).toInt())) {
            if(grouping->isValid()) {
                changeGrouping(grouping.value());
            }
        }
    }

    if(layout.contains("State"_L1)) {
        m_pendingState = QByteArray::fromBase64(layout.value("State"_L1).toString().toUtf8());
        if(!m_pendingState.isEmpty() && m_config.restoreState) {
            m_libraryTree->setLoading(true);
        }
    }
}

void LibraryTreeWidget::searchEvent(const SearchRequest& request)
{
    searchChanged(request);
}

LibraryTreeWidget::ConfigData LibraryTreeWidget::factoryConfig() const
{
    return {};
}

LibraryTreeWidget::ConfigData LibraryTreeWidget::defaultConfig() const
{
    auto config{factoryConfig()};

    config.doubleClickAction = m_settings->fileValue(LibTreeDoubleClickKey, config.doubleClickAction).toInt();
    config.middleClickAction = m_settings->fileValue(LibTreeMiddleClickKey, config.middleClickAction).toInt();
    config.sendPlayback      = m_settings->fileValue(LibTreeSendPlaybackKey, config.sendPlayback).toBool();
    config.playlistEnabled   = m_settings->fileValue(LibTreePlaylistEnabledKey, config.playlistEnabled).toBool();
    config.autoSwitch        = m_settings->fileValue(LibTreeAutoSwitchKey, config.autoSwitch).toBool();
    config.keepAlive         = m_settings->fileValue(LibTreeKeepAliveKey, config.keepAlive).toBool();
    config.playlistName      = m_settings->fileValue(LibTreeAutoPlaylistKey, config.playlistName).toString();
    config.restoreState      = m_settings->fileValue(LibTreeRestoreStateKey, config.restoreState).toBool();
    config.expandOnSingleClick
        = m_settings->fileValue(LibTreeExpandSingleClickKey, config.expandOnSingleClick).toBool();
    config.autoExpandSearchResultLimit
        = m_settings->fileValue(LibTreeAutoExpandSearchLimitKey, config.autoExpandSearchResultLimit).toInt();
    config.animated         = m_settings->fileValue(LibTreeAnimatedKey, config.animated).toBool();
    config.showHeader       = m_settings->fileValue(LibTreeHeaderKey, config.showHeader).toBool();
    config.showScrollbar    = m_settings->fileValue(LibTreeScrollBarKey, config.showScrollbar).toBool();
    config.alternatingRows  = m_settings->fileValue(LibTreeAltColoursKey, config.alternatingRows).toBool();
    config.showSummaryNode  = m_settings->fileValue(LibTreeShowSummaryNodeKey, config.showSummaryNode).toBool();
    config.summaryNodeTitle = m_settings->fileValue(LibTreeSummaryNodeTitleKey, config.summaryNodeTitle).toString();
    config.rowHeight        = m_settings->fileValue(LibTreeRowHeightKey, config.rowHeight).toInt();
    config.iconSize         = m_settings->value<LibTreeIconSize>().toSize();

    return config;
}

const LibraryTreeWidget::ConfigData& LibraryTreeWidget::currentConfig() const
{
    return m_config;
}

void LibraryTreeWidget::saveDefaults(const ConfigData& config) const
{
    m_settings->fileSet(LibTreeDoubleClickKey, config.doubleClickAction);
    m_settings->fileSet(LibTreeMiddleClickKey, config.middleClickAction);
    m_settings->fileSet(LibTreeSendPlaybackKey, config.sendPlayback);
    m_settings->fileSet(LibTreePlaylistEnabledKey, config.playlistEnabled);
    m_settings->fileSet(LibTreeAutoSwitchKey, config.autoSwitch);
    m_settings->fileSet(LibTreeKeepAliveKey, config.keepAlive);
    m_settings->fileSet(LibTreeAutoPlaylistKey, config.playlistName);
    m_settings->fileSet(LibTreeRestoreStateKey, config.restoreState);
    m_settings->fileSet(LibTreeExpandSingleClickKey, config.expandOnSingleClick);
    m_settings->fileSet(LibTreeAutoExpandSearchLimitKey, config.autoExpandSearchResultLimit);
    m_settings->fileSet(LibTreeAnimatedKey, config.animated);
    m_settings->fileSet(LibTreeHeaderKey, config.showHeader);
    m_settings->fileSet(LibTreeScrollBarKey, config.showScrollbar);
    m_settings->fileSet(LibTreeAltColoursKey, config.alternatingRows);
    m_settings->fileSet(LibTreeShowSummaryNodeKey, config.showSummaryNode);
    m_settings->fileSet(LibTreeSummaryNodeTitleKey, config.summaryNodeTitle);
    m_settings->fileSet(LibTreeRowHeightKey, config.rowHeight);
    m_settings->set<LibTreeIconSize>(config.iconSize);
}

void LibraryTreeWidget::clearSavedDefaults() const
{
    m_settings->fileRemove(LibTreeDoubleClickKey);
    m_settings->fileRemove(LibTreeMiddleClickKey);
    m_settings->fileRemove(LibTreeSendPlaybackKey);
    m_settings->fileRemove(LibTreePlaylistEnabledKey);
    m_settings->fileRemove(LibTreeAutoSwitchKey);
    m_settings->fileRemove(LibTreeKeepAliveKey);
    m_settings->fileRemove(LibTreeAutoPlaylistKey);
    m_settings->fileRemove(LibTreeRestoreStateKey);
    m_settings->fileRemove(LibTreeExpandSingleClickKey);
    m_settings->fileRemove(LibTreeAutoExpandSearchLimitKey);
    m_settings->fileRemove(LibTreeAnimatedKey);
    m_settings->fileRemove(LibTreeHeaderKey);
    m_settings->fileRemove(LibTreeScrollBarKey);
    m_settings->fileRemove(LibTreeAltColoursKey);
    m_settings->fileRemove(LibTreeShowSummaryNodeKey);
    m_settings->fileRemove(LibTreeSummaryNodeTitleKey);
    m_settings->fileRemove(LibTreeRowHeightKey);
    m_settings->reset<LibTreeIconSize>();
}

void LibraryTreeWidget::applyConfig(const ConfigData& config)
{
    m_config = config;

    if(!m_config.iconSize.isValid()) {
        m_config.iconSize = factoryConfig().iconSize;
    }

    m_config.rowHeight                   = std::max(m_config.rowHeight, 0);
    m_config.autoExpandSearchResultLimit = std::max(m_config.autoExpandSearchResultLimit, 0);

    if(m_config.summaryNodeTitle.isEmpty()) {
        m_config.summaryNodeTitle = factoryConfig().summaryNodeTitle;
    }

    m_doubleClickAction = static_cast<TrackAction>(m_config.doubleClickAction);
    m_middleClickAction = static_cast<TrackAction>(m_config.middleClickAction);

    m_libraryTree->setExpandsOnDoubleClick(m_doubleClickAction == TrackAction::None
                                           || m_doubleClickAction == TrackAction::Play);
    m_libraryTree->setExpandsOnSingleClick(m_config.expandOnSingleClick);
    m_trackSelection->changePlaybackOnSend(m_widgetContext, m_config.sendPlayback);
    m_libraryTree->setAnimated(m_config.animated);
    m_libraryTree->setHeaderHidden(!m_config.showHeader);
    setScrollbarEnabled(m_config.showScrollbar);
    m_libraryTree->setAlternatingRowColors(m_config.alternatingRows);
    m_model->setRowHeight(m_config.rowHeight);
    m_model->setSummaryNodeConfig(m_config.showSummaryNode, m_config.summaryNodeTitle);
    m_libraryTree->setIconSize(m_config.iconSize);

    QMetaObject::invokeMethod(m_libraryTree->itemDelegate(), "sizeHintChanged", Q_ARG(QModelIndex, {}));
}

void LibraryTreeWidget::contextMenuEvent(QContextMenuEvent* event)
{
    auto* menu = new QMenu(this);
    menu->setAttribute(Qt::WA_DeleteOnClose);
    Utils::forwardMenuStatusTips(menu);

    populateContextMenu(menu);
    menu->popup(event->globalPos());
}

void LibraryTreeWidget::keyPressEvent(QKeyEvent* event)
{
    const auto key = event->key();
    if(key == Qt::Key_Enter || key == Qt::Key_Return) {
        handleDoubleClick(m_libraryTree->currentIndex());
    }

    FyWidget::keyPressEvent(event);
}

LibraryTreeWidget::ConfigData LibraryTreeWidget::configFromLayout(const QJsonObject& layout) const
{
    ConfigData config{defaultConfig()};

    if(layout.contains("DoubleClickAction"_L1)) {
        config.doubleClickAction = layout.value("DoubleClickAction"_L1).toInt();
    }
    if(layout.contains("MiddleClickAction"_L1)) {
        config.middleClickAction = layout.value("MiddleClickAction"_L1).toInt();
    }
    if(layout.contains("SendPlayback"_L1)) {
        config.sendPlayback = layout.value("SendPlayback"_L1).toBool();
    }
    if(layout.contains("PlaylistEnabled"_L1)) {
        config.playlistEnabled = layout.value("PlaylistEnabled"_L1).toBool();
    }
    if(layout.contains("AutoSwitch"_L1)) {
        config.autoSwitch = layout.value("AutoSwitch"_L1).toBool();
    }
    if(layout.contains("KeepAlive"_L1)) {
        config.keepAlive = layout.value("KeepAlive"_L1).toBool();
    }
    if(layout.contains("PlaylistName"_L1)) {
        config.playlistName = layout.value("PlaylistName"_L1).toString();
    }
    if(layout.contains("RestoreState"_L1)) {
        config.restoreState = layout.value("RestoreState"_L1).toBool();
    }
    if(layout.contains("ExpandOnSingleClick"_L1)) {
        config.expandOnSingleClick = layout.value("ExpandOnSingleClick"_L1).toBool();
    }
    if(layout.contains("AutoExpandSearchResultLimit"_L1)) {
        config.autoExpandSearchResultLimit = layout.value("AutoExpandSearchResultLimit"_L1).toInt();
    }
    if(layout.contains("Animated"_L1)) {
        config.animated = layout.value("Animated"_L1).toBool();
    }
    if(layout.contains("ShowHeader"_L1)) {
        config.showHeader = layout.value("ShowHeader"_L1).toBool();
    }
    if(layout.contains("ShowScrollbar"_L1)) {
        config.showScrollbar = layout.value("ShowScrollbar"_L1).toBool();
    }
    if(layout.contains("AlternatingRows"_L1)) {
        config.alternatingRows = layout.value("AlternatingRows"_L1).toBool();
    }
    if(layout.contains("ShowSummaryNode"_L1)) {
        config.showSummaryNode = layout.value("ShowSummaryNode"_L1).toBool();
    }
    if(layout.contains("SummaryNodeTitle"_L1)) {
        config.summaryNodeTitle = layout.value("SummaryNodeTitle"_L1).toString();
    }
    if(layout.contains("RowHeight"_L1)) {
        config.rowHeight = layout.value("RowHeight"_L1).toInt();
    }
    if(layout.contains("IconWidth"_L1) && layout.contains("IconHeight"_L1)) {
        config.iconSize = {layout.value("IconWidth"_L1).toInt(), layout.value("IconHeight"_L1).toInt()};
    }

    if(!config.iconSize.isValid()) {
        config.iconSize = factoryConfig().iconSize;
    }

    config.rowHeight                   = std::max(config.rowHeight, 0);
    config.autoExpandSearchResultLimit = std::max(config.autoExpandSearchResultLimit, 0);

    if(config.summaryNodeTitle.isEmpty()) {
        config.summaryNodeTitle = factoryConfig().summaryNodeTitle;
    }

    return config;
}

void LibraryTreeWidget::saveConfigToLayout(const ConfigData& config, QJsonObject& layout)
{
    layout["DoubleClickAction"_L1]           = config.doubleClickAction;
    layout["MiddleClickAction"_L1]           = config.middleClickAction;
    layout["SendPlayback"_L1]                = config.sendPlayback;
    layout["PlaylistEnabled"_L1]             = config.playlistEnabled;
    layout["AutoSwitch"_L1]                  = config.autoSwitch;
    layout["KeepAlive"_L1]                   = config.keepAlive;
    layout["PlaylistName"_L1]                = config.playlistName;
    layout["RestoreState"_L1]                = config.restoreState;
    layout["ExpandOnSingleClick"_L1]         = config.expandOnSingleClick;
    layout["AutoExpandSearchResultLimit"_L1] = config.autoExpandSearchResultLimit;
    layout["Animated"_L1]                    = config.animated;
    layout["ShowHeader"_L1]                  = config.showHeader;
    layout["ShowScrollbar"_L1]               = config.showScrollbar;
    layout["AlternatingRows"_L1]             = config.alternatingRows;
    layout["ShowSummaryNode"_L1]             = config.showSummaryNode;
    layout["SummaryNodeTitle"_L1]            = config.summaryNodeTitle;
    layout["RowHeight"_L1]                   = config.rowHeight;
    layout["IconWidth"_L1]                   = config.iconSize.width();
    layout["IconHeight"_L1]                  = config.iconSize.height();
}

void LibraryTreeWidget::openConfigDialog()
{
    showConfigDialog(new LibraryTreeConfigDialog(this, m_groupsRegistry, this));
}

void LibraryTreeWidget::setupConnections()
{
    const auto resetModel = [this]() {
        m_model->reset(m_library->libraryTracks());
    };

    QObject::connect(m_resetThrottler, &SignalThrottler::triggered, this, resetModel);

    QObject::connect(m_model, &LibraryTreeModel::dataUpdated, m_libraryTree, &QTreeView::dataChanged);
    QObject::connect(m_model, &LibraryTreeModel::modelLoaded, this, [this]() { restoreState(m_pendingState); });

    QObject::connect(m_libraryTree, &LibraryTreeView::doubleClicked, this,
                     [this](const QModelIndex& index) { handleDoubleClick(index); });
    QObject::connect(m_libraryTree, &LibraryTreeView::middleClicked, this,
                     [this](const QModelIndex& index) { handleMiddleClick(index); });
    QObject::connect(m_libraryTree->selectionModel(), &QItemSelectionModel::selectionChanged, this,
                     [this](const QItemSelection& selected, const QItemSelection& deselected) {
                         selectionChanged(selected, deselected);
                     });
    QObject::connect(m_libraryTree->header(), &QHeaderView::customContextMenuRequested, this,
                     [this](const QPoint& pos) { setupHeaderContextMenu(pos); });
    QObject::connect(m_libraryTree, &QAbstractItemView::iconSizeChanged, this, [this](const QSize& size) {
        if(size.isValid() && m_config.iconSize != size) {
            m_config.iconSize = size;
            QMetaObject::invokeMethod(m_libraryTree->itemDelegate(), "sizeHintChanged", Q_ARG(QModelIndex, {}));
        }
    });
    QObject::connect(m_groupsRegistry, &LibraryTreeGroupRegistry::groupingChanged, this,
                     [this](const LibraryTreeGrouping& changedGrouping) {
                         if(m_grouping.id == changedGrouping.id) {
                             changeGrouping(changedGrouping);
                         }
                     });

    QObject::connect(m_library, &MusicLibrary::tracksLoaded, this, [this]() { reset(); });
    QObject::connect(m_library, &MusicLibrary::tracksAdded, this,
                     [this](const TrackList& tracks) { handleTracksAdded(tracks); });
    QObject::connect(m_library, &MusicLibrary::tracksScanned, m_model,
                     [this](int /*id*/, const TrackList& tracks) { handleTracksAdded(tracks); });
    QObject::connect(m_library, &MusicLibrary::tracksMetadataChanged, this,
                     [this](const TrackList& tracks) { handleTracksUpdated(tracks); });
    QObject::connect(m_library, &MusicLibrary::tracksUpdated, this,
                     [this](const TrackList& tracks) { m_model->refreshTracks(tracks); });
    QObject::connect(m_library, &MusicLibrary::tracksDeleted, m_model, &LibraryTreeModel::removeTracks);
    QObject::connect(m_library, &MusicLibrary::tracksSorted, this, [this]() { reset(); });

    QObject::connect(m_playerController, &PlayerController::playStateChanged, this,
                     [this](Player::PlayState state) { m_model->setPlayState(state); });
    QObject::connect(m_playerController, &PlayerController::playlistTrackUpdated, this,
                     [this](const auto& track) { playlistTrackChanged(track); });
    QObject::connect(m_playlistHandler, &PlaylistHandler::activePlaylistChanged, this,
                     [this](auto* playlist) { activePlaylistChanged(playlist); });

    m_styleProvider->subscribe(this, [this](const ResolvedAppStyle& resolvedStyle) {
        Gui::refreshItemViewPalette(m_libraryTree, resolvedStyle.palette);
        m_model->resetPalette();
        reset();
    });

    m_settings->subscribe<Settings::Core::UseVariousForCompilations>(this, [this]() { reset(); });

    m_settings->subscribe<Settings::Gui::RatingFullStarSymbol>(this, resetModel);
    m_settings->subscribe<Settings::Gui::RatingHalfStarSymbol>(this, resetModel);
    m_settings->subscribe<Settings::Gui::RatingEmptyStarSymbol>(this, resetModel);
}

void LibraryTreeWidget::reset() const
{
    if(!m_styleProvider->isResolved()) {
        return;
    }

    m_resetThrottler->throttle();
}

void LibraryTreeWidget::changeGrouping(const LibraryTreeGrouping& newGrouping)
{
    if(std::exchange(m_grouping, newGrouping) != newGrouping) {
        m_model->changeGrouping(m_grouping);
        reset();
    }
}

void LibraryTreeWidget::activePlaylistChanged(Playlist* playlist) const
{
    if(!playlist || !m_playlist) {
        return;
    }

    if(playlist->id() != m_playlist->id()) {
        m_model->setPlayingPath({}, {});
    }
}

void LibraryTreeWidget::playlistTrackChanged(const PlaylistTrack& track)
{
    if(!m_playlist || m_playlist->id() != track.playlistId) {
        return;
    }

    if(m_playlistGroups.empty()) {
        m_model->setPlayingPath({}, track.track.uniqueFilepath());
        return;
    }

    auto groupIt = m_playlistGroups.upper_bound(track.indexInPlaylist);
    if(groupIt != m_playlistGroups.cbegin()) {
        --groupIt;
    }
    m_model->setPlayingPath(groupIt->second, track.track.uniqueFilepath());
}

void LibraryTreeWidget::addGroupMenu(QMenu* parent)
{
    auto* groupMenu = new QMenu(tr("Grouping"), parent);

    auto* treeGroups = new QActionGroup(groupMenu);

    const auto groups = m_groupsRegistry->items();
    for(const auto& group : groups) {
        auto* switchGroup = new QAction(group.name, groupMenu);
        QObject::connect(switchGroup, &QAction::triggered, this, [this, group]() { changeGrouping(group); });
        switchGroup->setCheckable(true);
        switchGroup->setChecked(m_grouping.id == group.id);
        groupMenu->addAction(switchGroup);
        treeGroups->addAction(switchGroup);
    }

    groupMenu->addSeparator();

    auto* manageGroupings = new QAction(tr("Manage groupings..."), groupMenu);
    QObject::connect(manageGroupings, &QAction::triggered, this, [this]() {
        auto* dialog = new LibraryTreeGroupEditorDialog(m_groupsRegistry, this);
        dialog->open();
    });
    groupMenu->addAction(manageGroupings);

    parent->addMenu(groupMenu);
}

void LibraryTreeWidget::addOpenMenu(QMenu* menu)
{
    const auto selected = m_libraryTree->selectionModel()->selectedRows();
    if(selected.size() != 1) {
        return;
    }

    if(m_grouping.id != 2) {
        // Only add if in folder structure view
        return;
    }

    const auto tracks = selected.front().data(LibraryTreeItem::Tracks).value<TrackList>();
    if(tracks.empty()) {
        return;
    }

    const auto libId   = tracks.front().libraryId();
    const auto library = m_library->libraryInfo(libId);
    if(!library) {
        return;
    }

    QString dir{library->path};
    QStringList parentDirs;

    QModelIndex index{selected.front()};
    while(index.isValid()) {
        parentDirs.prepend(index.data().toString());
        index = index.parent();
    }

    dir += "/"_L1 + parentDirs.join(u'/');
    const QFileInfo info{dir};
    if(info.exists() && info.isDir()) {
        auto* openFolder = new QAction(tr("Open folder"), this);
        QObject::connect(openFolder, &QAction::triggered, this, [dir]() { Utils::File::openDirectory(dir); });
        menu->addAction(openFolder);
    }
}

void LibraryTreeWidget::setScrollbarEnabled(bool enabled) const
{
    m_libraryTree->setVerticalScrollBarPolicy(enabled ? Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff);
}

void LibraryTreeWidget::setupHeaderContextMenu(const QPoint& pos)
{
    auto* menu = new QMenu(this);
    menu->setAttribute(Qt::WA_DeleteOnClose);

    addGroupMenu(menu);
    addConfigureAction(menu);

    menu->popup(m_libraryTree->header()->viewport()->mapToGlobal(pos));
}

void LibraryTreeWidget::populateContextMenu(QMenu* menu)
{
    const bool hasSelection = m_libraryTree->selectionModel()->hasSelection();

    ContextMenuUtils::renderStaticContextMenu(
        menu, ContextMenuIds::LibraryTree::DefaultItems, m_settings->value<ContextMenuLibraryTreeLayout>(),
        m_settings->value<ContextMenuLibraryTreeDisabledSections>(),
        [&](const auto& id, QMenu* targetMenu, const auto& sectionEnabled) {
            if(id == QLatin1StringView{ContextMenuIds::LibraryTree::Play}) {
                if(hasSelection && sectionEnabled(ContextMenuIds::LibraryTree::Play)) {
                    targetMenu->addAction(m_playAction);
                }
                return;
            }
            if(id == QLatin1StringView{Constants::Actions::AddToCurrent}) {
                if(!hasSelection || !sectionEnabled(Constants::Actions::AddToCurrent)) {
                    return;
                }

                if(auto* addCurrentCmd = m_actionManager->command(Constants::Actions::AddToCurrent)) {
                    targetMenu->addAction(addCurrentCmd->action());
                }
                return;
            }
            if(id == QLatin1StringView{Constants::Actions::AddToActive}) {
                if(!hasSelection || !sectionEnabled(Constants::Actions::AddToActive)) {
                    return;
                }

                if(auto* addActiveCmd = m_actionManager->command(Constants::Actions::AddToActive)) {
                    targetMenu->addAction(addActiveCmd->action());
                }
                return;
            }
            if(id == QLatin1StringView{Constants::Actions::SendToCurrent}) {
                if(!hasSelection || !sectionEnabled(Constants::Actions::SendToCurrent)) {
                    return;
                }

                if(auto* sendCurrentCmd = m_actionManager->command(Constants::Actions::SendToCurrent)) {
                    targetMenu->addAction(sendCurrentCmd->action());
                }
                return;
            }
            if(id == QLatin1StringView{Constants::Actions::SendToNew}) {
                if(!hasSelection || !sectionEnabled(Constants::Actions::SendToNew)) {
                    return;
                }

                if(auto* sendNewCmd = m_actionManager->command(Constants::Actions::SendToNew)) {
                    targetMenu->addAction(sendNewCmd->action());
                }
                return;
            }
            if(id == QLatin1StringView{Constants::Actions::AddToPlaylist}) {
                if(hasSelection && sectionEnabled(Constants::Actions::AddToPlaylist)) {
                    auto* playlistMenu = new QMenu(tr("Add to playlist"), targetMenu);
                    m_trackSelection->addTrackAddToPlaylistContextMenu(playlistMenu, m_widgetContext);
                    if(!playlistMenu->actions().empty()) {
                        targetMenu->addMenu(playlistMenu);
                    }
                    else {
                        playlistMenu->deleteLater();
                    }
                }
                return;
            }
            if(id == QLatin1StringView{Constants::Actions::AddToQueue}) {
                if(!hasSelection || !sectionEnabled(Constants::Actions::AddToQueue)) {
                    return;
                }

                if(auto* addQueueCmd = m_actionManager->command(Constants::Actions::AddToQueue)) {
                    targetMenu->addAction(addQueueCmd->action());
                }
                return;
            }
            if(id == QLatin1StringView{Constants::Actions::QueueNext}) {
                if(!hasSelection || !sectionEnabled(Constants::Actions::QueueNext)) {
                    return;
                }

                if(auto* queueNextCmd = m_actionManager->command(Constants::Actions::QueueNext)) {
                    targetMenu->addAction(queueNextCmd->action());
                }
                return;
            }
            if(id == QLatin1StringView{Constants::Actions::RemoveFromQueue}) {
                if(!hasSelection || !sectionEnabled(Constants::Actions::RemoveFromQueue)) {
                    return;
                }

                if(auto* removeQueueCmd = m_actionManager->command(Constants::Actions::RemoveFromQueue)) {
                    targetMenu->addAction(removeQueueCmd->action());
                }
                return;
            }
            if(id == QLatin1StringView{ContextMenuIds::LibraryTree::Grouping}) {
                if(sectionEnabled(ContextMenuIds::LibraryTree::Grouping)) {
                    addGroupMenu(targetMenu);
                }
                return;
            }
            if(id == QLatin1StringView{ContextMenuIds::LibraryTree::Configure}) {
                if(sectionEnabled(ContextMenuIds::LibraryTree::Configure)) {
                    addConfigureAction(targetMenu, false);
                }
            }
            if(id == QLatin1StringView{ContextMenuIds::LibraryTree::OpenFolder}) {
                if(hasSelection && sectionEnabled(ContextMenuIds::LibraryTree::OpenFolder)) {
                    addOpenMenu(targetMenu);
                }
                return;
            }
            if(id == QLatin1StringView{ContextMenuIds::LibraryTree::TrackActions}) {
                if(hasSelection && sectionEnabled(ContextMenuIds::LibraryTree::TrackActions)) {
                    m_trackSelection->addTrackContextMenu(targetMenu, m_widgetContext);
                }
                return;
            }
        });
}

void LibraryTreeWidget::applySelectedTracks(const TrackList& selectedTracks) const
{
    const TrackList tracks = Gui::sortTracksForLibraryViewerPlaylist(m_settings, selectedTracks);
    m_sortProxy->setDragTracks(tracks);

    TrackSelection selectionState;
    selectionState.tracks = tracks;

    m_trackSelection->changeSelectedTracks(m_widgetContext, selectionState);

    m_addToQueueAction->setEnabled(true);
    m_queueNextAction->setEnabled(true);

    const auto queuedTracks = m_playerController->playbackQueue().tracks();

    bool canDeque{false};
    if(!queuedTracks.empty()) {
        std::set<int> selectedTrackIds;
        for(const Track& track : tracks) {
            selectedTrackIds.insert(track.id());
        }

        canDeque = std::ranges::any_of(queuedTracks, [&selectedTrackIds](const PlaylistTrack& track) {
            return selectedTrackIds.contains(track.track.id());
        });
    }
    m_removeFromQueueAction->setVisible(canDeque);

    syncSelectionPlaylist(tracks);
}

void LibraryTreeWidget::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) const
{
    if(m_updating) {
        return;
    }

    if(selected.indexes().empty() && deselected.indexes().empty()) {
        return;
    }

    const QModelIndexList filteredIndexes = filterAncestors(m_libraryTree->selectionModel()->selectedRows());

    const auto summaryNodeIt = std::ranges::find_if(filteredIndexes, [](const QModelIndex& index) {
        return index.isValid() && index.data(LibraryTreeItem::Level).toInt() == -1;
    });
    if(summaryNodeIt != filteredIndexes.cend()) {
        const bool usingFilteredTracks
            = !m_currentSearch.isEmpty() || m_currentEmptySearchMode == EmptySearchMode::ShowAll;
        applySelectedTracks(usingFilteredTracks ? m_filteredTracks : m_library->libraryTracks());
        return;
    }

    const TrackList selectedTracks = tracksForIndexes(m_sortProxy, filteredIndexes);
    applySelectedTracks(selectedTracks);
}

void LibraryTreeWidget::syncSelectionPlaylist(const TrackList& tracks) const
{
    if(!m_config.playlistEnabled || tracks.empty()) {
        return;
    }

    const QString playlistName{m_config.playlistName};

    if(m_config.keepAlive) {
        if(const auto* activePlaylist = m_playlistHandler->activePlaylist();
           activePlaylist && activePlaylist->name() == playlistName) {
            const QString keepActiveName = playlistName + u" ("_s + tr("Playback") + u")"_s;

            if(auto* keepActivePlaylist = m_playlistHandler->playlistByName(keepActiveName)) {
                m_playlistHandler->movePlaylistTracks(activePlaylist->id(), keepActivePlaylist->id());
            }
            else {
                m_playlistHandler->renamePlaylist(activePlaylist->id(), keepActiveName);
            }
        }
    }

    if(auto* playlist = m_playlistHandler->createPlaylist(playlistName, tracks)) {
        playlist->changeCurrentIndex(-1);

        if(m_config.autoSwitch) {
            m_playlistController->changeCurrentPlaylist(playlist);
        }
    }
}

void LibraryTreeWidget::queueSelectedTracks(bool next) const
{
    const QModelIndexList selectedIndexes = m_libraryTree->selectionModel()->selectedIndexes();
    if(selectedIndexes.empty()) {
        return;
    }

    m_trackSelection->executeAction(next ? TrackAction::QueueNext : TrackAction::AddToQueue);
    m_removeFromQueueAction->setVisible(true);
}

void LibraryTreeWidget::dequeueSelectedTracks() const
{
    const QModelIndexList selectedIndexes = m_libraryTree->selectionModel()->selectedIndexes();
    if(selectedIndexes.empty()) {
        return;
    }

    const QModelIndexList leafNodes = filterLeafNodes(m_sortProxy, selectedIndexes);

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

    TrackList tracks;

    for(const QModelIndex& index : leafNodes) {
        const auto indexTracks = index.data(LibraryTreeItem::Tracks).value<TrackList>();
        tracks.insert(tracks.end(), indexTracks.cbegin(), indexTracks.cend());
    }

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

    m_playerController->dequeueTracks(tracks);
    m_removeFromQueueAction->setVisible(false);
}

void LibraryTreeWidget::searchChanged(const SearchRequest& request)
{
    if(m_currentSearch == request.text && m_currentEmptySearchMode == request.emptyMode) {
        return;
    }

    m_currentSearch          = request.text;
    m_currentEmptySearchMode = request.emptyMode;

    if(m_currentSearch.length() < 1) {
        if(request.emptyMode == EmptySearchMode::ShowAll) {
            m_filteredTracks = m_library->libraryTracks();
            m_model->reset(m_filteredTracks);
        }
        else {
            m_filteredTracks.clear();
            m_model->reset(m_library->libraryTracks());
        }
        return;
    }

    Utils::asyncExec([search = m_currentSearch, tracks = m_library->libraryTracks()]() {
        TrackQueryFilter filter;
        return filter.filter(search, tracks);
    }).then(this, [this, search = m_currentSearch](const TrackList& filteredTracks) {
        if(m_currentSearch != search) {
            return;
        }

        m_filteredTracks = filteredTracks;

        if(shouldAutoExpandSearchResults(m_filteredTracks)) {
            QObject::connect(m_model, &LibraryTreeModel::modelUpdated, this, &LibraryTreeWidget::expandSearchResults,
                             Qt::SingleShotConnection);
        }

        m_model->reset(m_filteredTracks);
    });
}

bool LibraryTreeWidget::shouldAutoExpandSearchResults(const TrackList& tracks) const
{
    if(m_config.autoExpandSearchResultLimit <= 0 || m_currentSearch.isEmpty() || tracks.empty()) {
        return false;
    }

    return std::cmp_less_equal(tracks.size(), m_config.autoExpandSearchResultLimit);
}

void LibraryTreeWidget::expandSearchResults()
{
    if(!shouldAutoExpandSearchResults(m_filteredTracks)) {
        return;
    }

    const auto expandChildren = [this](auto&& self, const QModelIndex& sourceParent) -> void {
        m_model->ensureChildrenAvailable(sourceParent);

        const int rowCount = m_model->rowCount(sourceParent);
        for(int row{0}; row < rowCount; ++row) {
            const QModelIndex sourceChild = m_model->index(row, 0, sourceParent);
            if(!sourceChild.isValid() || !m_model->hasChildren(sourceChild)) {
                continue;
            }

            m_model->ensureChildrenAvailable(sourceChild);

            const QModelIndex proxyChild = m_sortProxy->mapFromSource(sourceChild);
            if(proxyChild.isValid()) {
                m_libraryTree->expand(proxyChild);
            }

            self(self, sourceChild);
        }
    };

    expandChildren(expandChildren, {});
}

void LibraryTreeWidget::handlePlayback(const QModelIndexList& indexes, int row)
{
    m_playlistGroups.clear();

    const QModelIndexList leafNodes = filterLeafNodes(m_sortProxy, indexes);

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

    TrackList tracks;

    QModelIndex parent;
    for(const QModelIndex& index : leafNodes) {
        if(index.data(LibraryTreeItem::TrackCount).toInt() != 1) {
            continue;
        }

        if(!parent.isValid()) {
            parent = index.parent();
        }
        else if(index.parent() != parent) {
            if(m_playlistGroups.empty()) {
                m_playlistGroups[0] = parent.data(Qt::DisplayRole).toString();
            }
            parent = index.parent();

            m_playlistGroups[static_cast<int>(tracks.size())] = parent.data(Qt::DisplayRole).toString();
        }
        const auto indexTracks = index.data(LibraryTreeItem::Tracks).value<TrackList>();
        tracks.insert(tracks.end(), indexTracks.cbegin(), indexTracks.cend());
    }

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

    syncSelectionPlaylist(tracks);

    if(parent.isValid() && m_playlistGroups.empty()) {
        m_playlistGroups[0] = parent.data(Qt::DisplayRole).toString();
    }

    m_playlist = m_playlistHandler->createTempPlaylist(QString::fromLatin1(LibTreePlaylist), tracks);
    if(m_playlist) {
        m_playlist->changeCurrentIndex(row);
        m_playerController->startPlayback(m_playlist);
    }
}

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

    if(m_sortProxy->hasChildren(index)) {
        return;
    }

    const int row            = index.row();
    const QModelIndex parent = index.parent();
    const int count          = m_sortProxy->rowCount(parent);

    QModelIndexList trackIndexes;

    for(int i{0}; i < count; ++i) {
        trackIndexes.emplace_back(m_sortProxy->index(i, 0, parent));
    }

    handlePlayback(trackIndexes, row);
}

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

    if(m_doubleClickAction == TrackAction::None) {
        return;
    }

    if(m_doubleClickAction == TrackAction::Play) {
        handlePlayTrack(index);
        return;
    }

    PlaylistAction::ActionOptions options;

    if(m_config.autoSwitch) {
        options |= PlaylistAction::Switch;
    }
    if(m_config.sendPlayback) {
        options |= PlaylistAction::StartPlayback;
    }

    m_trackSelection->executeAction(m_doubleClickAction, options);
    m_removeFromQueueAction->setVisible(m_doubleClickAction == TrackAction::AddToQueue
                                        || m_doubleClickAction == TrackAction::SendToQueue);
}

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

    PlaylistAction::ActionOptions options;

    if(m_config.autoSwitch) {
        options |= PlaylistAction::Switch;
    }
    if(m_config.sendPlayback) {
        options |= PlaylistAction::StartPlayback;
    }

    m_trackSelection->executeAction(m_middleClickAction, options);
    m_removeFromQueueAction->setVisible(m_middleClickAction == TrackAction::AddToQueue
                                        || m_middleClickAction == TrackAction::SendToQueue);
}

void LibraryTreeWidget::handleTracksAdded(const TrackList& tracks)
{
    if(tracks.empty()) {
        return;
    }

    if(!m_currentSearch.isEmpty()) {
        Utils::asyncExec([search = m_currentSearch, tracks]() {
            TrackQueryFilter filter;
            return filter.filter(search, tracks);
        }).then(this, [this, search = m_currentSearch](const TrackList& filteredTracks) {
            if(m_currentSearch != search) {
                return;
            }

            m_model->addTracks(filteredTracks);
        });
    }
    else {
        m_model->addTracks(tracks);
    }
}

void LibraryTreeWidget::handleTracksUpdated(const TrackList& tracks)
{
    if(tracks.empty()) {
        return;
    }

    m_updating = true;

    const QModelIndexList selectedRows = m_libraryTree->selectionModel()->selectedRows();
    const QModelIndexList trackParents = m_model->indexesForTracks(tracks);

    std::vector<Md5Hash> selectedKeys;
    selectedKeys.reserve(selectedRows.size());
    for(const QModelIndex& index : selectedRows) {
        const auto key = index.data(LibraryTreeItem::Key).toByteArray();
        if(!key.isEmpty()) {
            selectedKeys.emplace_back(key);
        }
    }

    std::stack<QModelIndex> checkExpanded;
    for(const QModelIndex& index : trackParents) {
        if(index.isValid()) {
            checkExpanded.emplace(index);
        }
    }

    std::vector<Md5Hash> expandedKeys;
    QSet<Md5Hash> expandedKeySet;
    while(!checkExpanded.empty()) {
        const QModelIndex index = checkExpanded.top();
        checkExpanded.pop();
        if(!index.isValid()) {
            continue;
        }

        if(m_libraryTree->isExpanded(index)) {
            const auto key = index.data(LibraryTreeItem::Key).toByteArray();
            if(!key.isEmpty() && !expandedKeySet.contains(key)) {
                expandedKeys.emplace_back(key);
                expandedKeySet.insert(key);
            }
            const int rowCount = m_sortProxy->rowCount(index);
            for(int row{0}; row < rowCount; ++row) {
                checkExpanded.emplace(m_sortProxy->index(row, 0, index));
            }
        }
    }

    QObject::connect(
        m_model, &LibraryTreeModel::modelUpdated, this,
        [this, expandedKeys, selectedKeys]() { restoreSelection(expandedKeys, selectedKeys); },
        Qt::SingleShotConnection);

    m_model->updateTracks(tracks);
}

void LibraryTreeWidget::restoreSelection(const std::vector<Md5Hash>& expandedKeys,
                                         const std::vector<Md5Hash>& selectedKeys)
{
    const QModelIndexList expandedIndexes = m_model->indexesForKeys(expandedKeys);
    const QModelIndexList selectedIndexes = m_model->indexesForKeys(selectedKeys);

    for(const QModelIndex& index : expandedIndexes) {
        if(index.isValid()) {
            m_libraryTree->setExpanded(m_sortProxy->mapFromSource(index), true);
        }
    }

    QItemSelection indexesToSelect;
    indexesToSelect.reserve(selectedIndexes.size());

    for(const QModelIndex& index : selectedIndexes) {
        if(index.isValid()) {
            const QModelIndex proxyIndex = m_sortProxy->mapFromSource(index);
            indexesToSelect.append({proxyIndex, proxyIndex});
        }
    }

    m_libraryTree->selectionModel()->select(indexesToSelect, QItemSelectionModel::ClearAndSelect);
    m_updating = false;
}

QByteArray LibraryTreeWidget::saveState() const
{
    QByteArray stateData;
    QDataStream stream{&stateData, QIODeviceBase::WriteOnly};
    stream.setVersion(QDataStream::Qt_6_0);

    const QModelIndex topIndex = m_libraryTree->indexAt({0, 0});
    const auto topKey          = topIndex.data(LibraryTreeItem::Key).toByteArray();
    stream << topKey;

    std::vector<QByteArray> keysToSave;
    std::stack<QModelIndex> indexes;
    indexes.emplace();

    while(!indexes.empty()) {
        const QModelIndex index = indexes.top();
        indexes.pop();

        if(m_libraryTree->isExpanded(index) || !index.isValid()) {
            if(index.isValid()) {
                keysToSave.emplace_back(index.data(LibraryTreeItem::Key).toByteArray());
            }

            const int childCount = m_sortProxy->rowCount(index);
            for(int row{0}; row < childCount; ++row) {
                indexes.push(m_sortProxy->index(row, 0, index));
            }
        }
    }

    if(!keysToSave.empty()) {
        stream << keysToSave;
    }

    stateData = qCompress(stateData, 9);
    return stateData;
}

void LibraryTreeWidget::restoreIndexState(const QByteArray& topKey, const std::vector<QByteArray>& keys,
                                          int currentIndex)
{
    // We use queued connections here so any expand calls have a chance to load their children in fetchMore
    if(std::cmp_greater_equal(currentIndex, keys.size())) {
        QMetaObject::invokeMethod(
            m_libraryTree,
            [this, topKey]() {
                if(const QModelIndex topIndex = m_model->indexForKey(topKey); topIndex.isValid()) {
                    m_libraryTree->scrollTo(m_sortProxy->mapFromSource(topIndex), QAbstractItemView::PositionAtTop);
                }
                m_libraryTree->setLoading(false);
                m_libraryTree->setAnimated(m_config.animated);
            },
            Qt::QueuedConnection);

        return;
    }

    const auto& key = keys.at(currentIndex);
    if(const auto index = m_model->indexForKey(key); index.isValid()) {
        m_model->ensureChildrenAvailable(index);
        m_libraryTree->expand(m_sortProxy->mapFromSource(index));
        QMetaObject::invokeMethod(
            m_libraryTree, [this, topKey, keys, currentIndex]() { restoreIndexState(topKey, keys, currentIndex + 1); },
            Qt::QueuedConnection);
    }
    else {
        restoreIndexState(topKey, keys, currentIndex + 1);
    }
}

void LibraryTreeWidget::restoreState(const QByteArray& state)
{
    if(state.isEmpty() || !m_config.restoreState) {
        return;
    }

    // Disable animation during state restore to prevent visual artifacts
    m_libraryTree->setAnimated(false);

    QByteArray stateData = qUncompress(state);
    QDataStream stream{&stateData, QIODeviceBase::ReadOnly};
    stream.setVersion(QDataStream::Qt_6_0);

    QByteArray topKey;
    stream >> topKey;

    std::vector<QByteArray> keysToRestore;
    stream >> keysToRestore;

    restoreIndexState(topKey, keysToRestore);
}
} // namespace Fooyin

#include "moc_librarytreewidget.cpp"
