/*
 * Fooyin
 * Copyright © 2022, 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 <core/playlist/playlisthandler.h>

#include "application.h"
#include "database/playlistdatabase.h"
#include "internalcoresettings.h"
#include "playback/playbackstatestore.h"
#include "playback/playlistnavigator.h"

#include <core/coresettings.h>
#include <core/library/libraryutils.h>
#include <core/library/musiclibrary.h>
#include <core/player/playerdefs.h>
#include <core/playlist/playlist.h>
#include <utils/crypto.h>
#include <utils/helpers.h>
#include <utils/settings/settingsmanager.h>

#include <QFileInfo>
#include <QLoggingCategory>

#include <deque>
#include <ranges>
#include <set>
#include <unordered_map>
#include <utility>

Q_LOGGING_CATEGORY(PL_HANDLER, "fy.playlisthandler")

using namespace Qt::StringLiterals;

namespace Fooyin {
namespace {
using TrackKeySet        = std::unordered_set<QString>;
using RemovedPlaylistMap = std::unordered_map<QString, Playlist*>;

QString removedPlaylistKey(const QString& name)
{
    return name.toCaseFolded();
}

TrackKeySet playlistTrackKeySet(const TrackList& tracks)
{
    TrackKeySet result;
    result.reserve(tracks.size());
    for(const auto& track : tracks) {
        result.emplace(track.identityKey());
    }
    return result;
}

enum class PreservationMode : uint8_t
{
    Preserve,
    Ignore,
};

PlaylistTrackList rebuildPlaylistTracks(Playlist* playlist, const TrackList& tracks,
                                        PreservationMode mode = PreservationMode::Ignore)
{
    if(!playlist) {
        return {};
    }

    if(mode == PreservationMode::Ignore) {
        return PlaylistTrack::fromTracks(tracks, playlist->id());
    }

    const PlaylistTrackList oldTracks = playlist->playlistTracks();

    std::unordered_map<QString, std::deque<UId>> preservedEntries;
    preservedEntries.reserve(oldTracks.size());

    for(const auto& oldTrack : oldTracks) {
        preservedEntries[oldTrack.track.identityKey()].push_back(oldTrack.entryId);
    }

    PlaylistTrackList result;
    result.reserve(tracks.size());

    for(int index{0}; const auto& track : tracks) {
        const QString key      = track.identityKey();
        const auto preservedIt = preservedEntries.find(key);
        const UId entryId      = preservedIt != preservedEntries.cend() && !preservedIt->second.empty()
                                   ? preservedIt->second.front()
                                   : UId::create();
        if(preservedIt != preservedEntries.cend() && !preservedIt->second.empty()) {
            preservedIt->second.pop_front();
        }
        result.emplace_back(track, playlist->id(), entryId, index++);
    }

    return result;
}

bool playlistTracksSameData(const PlaylistTrackList& lhs, const PlaylistTrackList& rhs)
{
    if(lhs.size() != rhs.size()) {
        return false;
    }

    return std::ranges::equal(lhs, rhs, [](const PlaylistTrack& left, const PlaylistTrack& right) {
        return left.playlistId == right.playlistId && left.entryId == right.entryId
            && left.indexInPlaylist == right.indexInPlaylist && left.track.sameDataAs(right.track);
    });
}
} // namespace

using PlaylistPtrList = std::vector<std::unique_ptr<Playlist>>;

class PlaylistHandlerPrivate
{
public:
    PlaylistHandlerPrivate(PlaylistHandler* self, DbConnectionPoolPtr dbPool, std::shared_ptr<AudioLoader> audioLoader,
                           MusicLibrary* library, SettingsManager* settings);

    void reloadPlaylists();
    void populatePlaylists();
    void regenerateAutoPlaylists(const TrackList& updatedTracks = {});
    bool noConcretePlaylists();

    void handleTracksChanged(const TrackList& tracks);
    void handleTracksDeleted(const TrackList& tracks);
    void handleTracksUpdated(const TrackList& tracks);

    void prepareUpcomingTrack();
    [[nodiscard]] PlaylistTrack currentTrack() const;
    [[nodiscard]] PlaylistTrack nextTrackChange(int delta, Playlist::PlayModes mode);
    [[nodiscard]] PlaylistTrack nextTrack(int delta, Playlist::PlayModes mode) const;

    void resetShuffleOrder();
    void updateIndices();
    void savePlaylists();
    PlaylistTrackList ensureDatabaseTracks(Playlist* playlist, const PlaylistTrackList& tracks) const;
    void replacePlaylistTracks(Playlist* playlist, const PlaylistTrackList& tracks,
                               PlaylistTrackChangeSource source           = PlaylistTrackChangeSource::External,
                               const TrackEntryIdSet& updatedTrackEntries = {});
    void cancelPendingRemovedPlaylist(const QString& name);
    void trackPendingRemovedPlaylist(Playlist* playlist);

    [[nodiscard]] QString findUniqueName(const QString& name) const;
    [[nodiscard]] int indexFromName(const PlaylistPtrList& list, const QString& name) const;
    [[nodiscard]] int nextValidIndex() const;
    [[nodiscard]] bool validId(const UId& id) const;
    [[nodiscard]] bool validName(const QString& name) const;
    [[nodiscard]] bool validIndex(int index) const;

    void restoreActivePlaylist();
    Playlist* addNewPlaylist(const QString& name, bool isTemporary = false);
    Playlist* addNewAutoPlaylist(const QString& name, const QString& query, const QString& sortQuery, bool forceSorted);

    PlaylistHandler* m_self;

    DbConnectionPoolPtr m_dbPool;
    PlaylistNavigator m_playlistNavigator;
    MusicLibrary* m_library;
    SettingsManager* m_settings;
    PlaylistDatabase m_playlistConnector;

    PlaylistPtrList m_playlists;
    PlaylistPtrList m_removedPlaylists;
    RemovedPlaylistMap m_pendingRemovedPlaylists;

    Playlist* m_activePlaylist{nullptr};
};

PlaylistHandlerPrivate::PlaylistHandlerPrivate(PlaylistHandler* self, DbConnectionPoolPtr dbPool,
                                               std::shared_ptr<AudioLoader> audioLoader, MusicLibrary* library,
                                               SettingsManager* settings)
    : m_self{self}
    , m_dbPool{std::move(dbPool)}
    , m_playlistNavigator{std::move(audioLoader)}
    , m_library{library}
    , m_settings{settings}
{
    const DbConnectionProvider dbProvider{m_dbPool};
    m_playlistConnector.initialise(dbProvider);
}

void PlaylistHandlerPrivate::reloadPlaylists()
{
    const std::vector<PlaylistInfo> infos = m_playlistConnector.getAllPlaylists();

    for(const auto& info : infos) {
        Playlist* playlist{nullptr};

        if(info.isAutoPlaylist) {
            playlist = m_playlists
                           .emplace_back(Playlist::createAuto(info.dbId, info.name, info.index, info.query,
                                                              info.sortQuery, info.forceSorted, m_settings))
                           .get();
        }
        else {
            playlist = m_playlists.emplace_back(Playlist::create(info.dbId, info.name, info.index, m_settings)).get();
        }

        if(playlist) {
            playlist->storeExtraProperties(info.extraProperties);
        }
    }
}

void PlaylistHandlerPrivate::populatePlaylists()
{
    std::unordered_map<int, Track> idTracks;

    const TrackList tracks = m_library->tracks();
    for(const Track& track : tracks) {
        idTracks.emplace(track.id(), track);
    }

    for(const auto& playlist : m_playlists) {
        if(playlist->isAutoPlaylist()) {
            if(!playlist->forceSorted()) {
                const TrackList playlistTracks = m_playlistConnector.getPlaylistTracks(*playlist, idTracks);
                playlist->replaceTracks(playlistTracks);
                playlist->setTracksModified(false);
            }
            playlist->regenerateTracks(tracks);
        }
        else {
            const TrackList playlistTracks = m_playlistConnector.getPlaylistTracks(*playlist, idTracks);
            playlist->replaceTracks(playlistTracks);
        }
    }

    restoreActivePlaylist();

    Q_EMIT m_self->playlistsPopulated();
}

void PlaylistHandlerPrivate::regenerateAutoPlaylists(const TrackList& updatedTracks)
{
    const TrackList tracks            = m_library->tracks();
    const TrackKeySet updatedTrackIds = playlistTrackKeySet(updatedTracks);

    for(auto& playlist : m_playlists) {
        if(!playlist->isAutoPlaylist()) {
            continue;
        }

        const PlaylistTrackList oldTracks = playlist->playlistTracks();
        const TrackList regeneratedTracks = playlist->autoPlaylistTracks(tracks);
        const PlaylistTrackList newTracks
            = rebuildPlaylistTracks(playlist.get(), regeneratedTracks, PreservationMode::Preserve);

        TrackEntryIdSet updatedEntries;
        updatedEntries.reserve(oldTracks.size());

        for(const auto& track : oldTracks) {
            if(updatedTrackIds.contains(track.track.identityKey())) {
                updatedEntries.emplace(track.entryId);
            }
        }

        const auto changeSet = buildPlaylistChangeset(oldTracks, newTracks, updatedEntries);
        if(changeSet && changeSet->isEmpty()) {
            continue;
        }

        playlist->replaceTracks(newTracks);

        if(changeSet) {
            Q_EMIT m_self->tracksPatched(playlist.get(), changeSet.value(), PlaylistTrackChangeSource::External);
        }
        else {
            Q_EMIT m_self->tracksChanged(playlist.get(), {}, PlaylistTrackChangeSource::External);
        }
    }
}

bool PlaylistHandlerPrivate::noConcretePlaylists()
{
    return m_playlists.empty()
        || std::ranges::all_of(m_playlists, [](const auto& playlist) { return playlist->isTemporary(); });
}

void PlaylistHandlerPrivate::handleTracksChanged(const TrackList& tracks)
{
    for(auto& playlist : m_playlists) {
        if(playlist->isAutoPlaylist()) {
            continue;
        }

        TrackList playlistTracks  = playlist->tracks();
        const auto updatedIndexes = Utils::updateCommonTracks(playlistTracks, tracks, Utils::CommonOperation::Update);

        if(!updatedIndexes.empty()) {
            for(const int index : updatedIndexes) {
                playlist->updateTrackAtIndex(index, playlistTracks.at(index));
            }
            Q_EMIT m_self->tracksChanged(playlist.get(), updatedIndexes, PlaylistTrackChangeSource::External);
        }
    }
}

void PlaylistHandlerPrivate::handleTracksDeleted(const TrackList& tracks)
{
    if(tracks.empty()) {
        return;
    }

    const TrackKeySet deletedTrackKeys = playlistTrackKeySet(tracks);

    for(auto& playlist : m_playlists) {
        if(playlist->isAutoPlaylist()) {
            continue;
        }

        std::vector<int> indexesToRemove;
        indexesToRemove.reserve(playlist->trackCount());

        const TrackList playlistTracks = playlist->tracks();
        for(int i{0}; const auto& track : playlistTracks) {
            if(deletedTrackKeys.contains(track.uniqueFilepath())) {
                indexesToRemove.emplace_back(i);
            }
            ++i;
        }

        if(indexesToRemove.empty()) {
            continue;
        }

        const auto removedIndexes = playlist->removeTracks(indexesToRemove);
        Q_EMIT m_self->tracksRemoved(playlist.get(), removedIndexes);
    }
}

void PlaylistHandlerPrivate::handleTracksUpdated(const TrackList& tracks)
{
    for(auto& playlist : m_playlists) {
        if(playlist->isAutoPlaylist()) {
            continue;
        }

        TrackList playlistTracks  = playlist->tracks();
        const auto updatedIndexes = Utils::updateCommonTracks(playlistTracks, tracks, Utils::CommonOperation::Update);

        if(!updatedIndexes.empty()) {
            for(const int index : updatedIndexes) {
                playlist->updateTrackAtIndex(index, playlistTracks.at(index));
            }
            Q_EMIT m_self->tracksUpdated(playlist.get(), updatedIndexes);
        }
    }
}

PlaylistTrack PlaylistHandlerPrivate::currentTrack() const
{
    return m_playlistNavigator.currentTrack(m_activePlaylist);
}

PlaylistTrack PlaylistHandlerPrivate::nextTrackChange(int delta, Playlist::PlayModes mode)
{
    return m_playlistNavigator.advanceRelativeTrack(m_activePlaylist, mode, delta);
}

PlaylistTrack PlaylistHandlerPrivate::nextTrack(int delta, Playlist::PlayModes mode) const
{
    return m_playlistNavigator.previewRelativeTrack(m_activePlaylist, mode, delta);
}

void PlaylistHandlerPrivate::prepareUpcomingTrack()
{
    const auto mode = static_cast<Playlist::PlayModes>(m_settings->value<Settings::Core::PlayMode>());
    m_playlistNavigator.previewRelativeTrack(m_activePlaylist, mode, 1);
}

void PlaylistHandlerPrivate::resetShuffleOrder()
{
    for(auto& playlist : m_playlists) {
        (*playlist).reset();
    }
}

void PlaylistHandlerPrivate::updateIndices()
{
    for(int index{0}; auto& playlist : m_playlists) {
        if(!playlist->isTemporary()) {
            playlist->setIndex(index++);
        }
    }
}

void PlaylistHandlerPrivate::savePlaylists()
{
    PlaylistList playlistsToSave;

    for(const auto& playlist : m_playlists) {
        if(playlist->modified() || playlist->tracksModified()) {
            playlistsToSave.emplace_back(playlist.get());
        }
    }

    m_playlistConnector.saveModifiedPlaylists(playlistsToSave);

    if(!m_activePlaylist) {
        return;
    }

    if(m_activePlaylist->isTemporary()) {
        PlaybackState::clearActivePlaylistDbId();
    }
    else {
        PlaybackState::saveActivePlaylistDbId(m_activePlaylist->dbId());
    }
}

PlaylistTrackList PlaylistHandlerPrivate::ensureDatabaseTracks(Playlist* playlist,
                                                               const PlaylistTrackList& tracks) const
{
    if(!playlist || playlist->isTemporary()) {
        return tracks;
    }

    PlaylistTrackList persistentTracks{tracks};
    for(PlaylistTrack& track : persistentTracks) {
        track.track = m_playlistConnector.ensureTrack(track.track);
    }
    return persistentTracks;
}

void PlaylistHandlerPrivate::replacePlaylistTracks(Playlist* playlist, const PlaylistTrackList& tracks,
                                                   PlaylistTrackChangeSource source,
                                                   const TrackEntryIdSet& updatedTrackEntries)
{
    if(!playlist) {
        return;
    }

    const PlaylistTrackList oldTracks = playlist->playlistTracks();
    const PlaylistTrackList newTracks = PlaylistTrack::updateIndexes(ensureDatabaseTracks(playlist, tracks));
    if(playlistTracksSameData(oldTracks, newTracks)) {
        return;
    }

    const auto changeSet = buildPlaylistChangeset(oldTracks, newTracks, updatedTrackEntries);

    playlist->replaceTracks(newTracks);

    if(changeSet && !changeSet->isEmpty()) {
        Q_EMIT m_self->tracksPatched(playlist, *changeSet, source);
    }
    else {
        Q_EMIT m_self->tracksChanged(playlist, {}, source);
    }
}

void PlaylistHandlerPrivate::cancelPendingRemovedPlaylist(const QString& name)
{
    if(!name.isEmpty()) {
        m_pendingRemovedPlaylists.erase(removedPlaylistKey(name));
    }
}

void PlaylistHandlerPrivate::trackPendingRemovedPlaylist(Playlist* playlist)
{
    if(playlist && !playlist->isTemporary()) {
        m_pendingRemovedPlaylists[removedPlaylistKey(playlist->name())] = playlist;
    }
}

QString PlaylistHandlerPrivate::findUniqueName(const QString& name) const
{
    return Utils::findUniqueString(name, m_playlists, [](const auto& playlist) { return playlist->name(); });
}

int PlaylistHandlerPrivate::indexFromName(const PlaylistPtrList& list, const QString& name) const
{
    if(name.isEmpty()) {
        return -1;
    }

    auto it = std::ranges::find_if(
        list, [name](const auto& playlist) { return playlist->name().compare(name, Qt::CaseInsensitive) == 0; });

    if(it == list.cend()) {
        return -1;
    }

    return static_cast<int>(std::ranges::distance(list.cbegin(), it));
}

int PlaylistHandlerPrivate::nextValidIndex() const
{
    if(m_playlists.empty()) {
        return 0;
    }

    auto indices  = m_playlists | std::ranges::views::transform([](const auto& playlist) { return playlist->index(); });
    auto maxIndex = std::ranges::max_element(indices);

    const int nextValidIndex = *maxIndex + 1;

    return nextValidIndex;
}

bool PlaylistHandlerPrivate::validId(const UId& id) const
{
    auto it
        = std::ranges::find_if(std::as_const(m_playlists), [id](const auto& playlist) { return playlist->id() == id; });
    return it != m_playlists.cend();
}

bool PlaylistHandlerPrivate::validName(const QString& name) const
{
    auto it = std::ranges::find_if(std::as_const(m_playlists),
                                   [name](const auto& playlist) { return playlist->name() == name; });
    return it != m_playlists.cend();
}

bool PlaylistHandlerPrivate::validIndex(int index) const
{
    return (index >= 0 && std::cmp_less(index, m_playlists.size()));
}

void PlaylistHandlerPrivate::restoreActivePlaylist()
{
    const auto lastId = PlaybackState::activePlaylistDbId();
    if(!lastId.has_value()) {
        return;
    }

    auto playlist
        = std::ranges::find_if(std::as_const(m_playlists), [lastId](const auto& pl) { return pl->dbId() == *lastId; });
    if(playlist == m_playlists.cend()) {
        return;
    }

    m_activePlaylist = playlist->get();
    Q_EMIT m_self->activePlaylistChanged(m_activePlaylist);
}

Playlist* PlaylistHandlerPrivate::addNewPlaylist(const QString& name, bool isTemporary)
{
    auto existingIndex = indexFromName(m_playlists, name);

    if(existingIndex >= 0) {
        return m_playlists.at(existingIndex).get();
    }

    if(isTemporary) {
        const QString tempName = !name.isEmpty() ? name : findUniqueName(u"TempPlaylist"_s);
        auto* playlist         = m_playlists.emplace_back(Playlist::create(tempName, m_settings)).get();
        return playlist;
    }

    const QString playlistName = !name.isEmpty() ? name : findUniqueName(u"Playlist"_s);
    cancelPendingRemovedPlaylist(playlistName);

    const int index = nextValidIndex();
    const int dbId  = m_playlistConnector.insertPlaylist(playlistName, index, false, {});

    if(dbId >= 0) {
        auto* playlist = m_playlists.emplace_back(Playlist::create(dbId, playlistName, index, m_settings)).get();
        return playlist;
    }

    return nullptr;
}

Playlist* PlaylistHandlerPrivate::addNewAutoPlaylist(const QString& name, const QString& query,
                                                     const QString& sortQuery, bool forceSorted)
{
    auto existingIndex = indexFromName(m_playlists, name);

    if(existingIndex >= 0) {
        return m_playlists.at(existingIndex).get();
    }

    const QString playlistName = !name.isEmpty() ? name : findUniqueName(u"Auto Playlist"_s);
    cancelPendingRemovedPlaylist(playlistName);

    const int index = nextValidIndex();
    const int dbId  = m_playlistConnector.insertPlaylist(playlistName, index, true, query, sortQuery, forceSorted);

    if(dbId >= 0) {
        auto* playlist = m_playlists
                             .emplace_back(Playlist::createAuto(dbId, playlistName, index, query, sortQuery,
                                                                forceSorted, m_settings))
                             .get();
        return playlist;
    }

    return nullptr;
}

PlaylistHandler::PlaylistHandler(DbConnectionPoolPtr dbPool, std::shared_ptr<AudioLoader> audioLoader,
                                 MusicLibrary* library, SettingsManager* settings, QObject* parent)
    : QObject{parent}
    , p{std::make_unique<PlaylistHandlerPrivate>(this, std::move(dbPool), std::move(audioLoader), library, settings)}
{
    p->reloadPlaylists();

    if(p->noConcretePlaylists()) {
        PlaylistHandler::createPlaylist(u"Default"_s, {});
    }

    QObject::connect(p->m_library, &MusicLibrary::tracksLoaded, this, [this]() { p->populatePlaylists(); });
    QObject::connect(p->m_library, &MusicLibrary::tracksAdded, this, [this]() { p->regenerateAutoPlaylists(); });
    QObject::connect(p->m_library, &MusicLibrary::tracksMetadataChanged, this, [this](const TrackList& tracks) {
        p->handleTracksChanged(tracks);
        p->regenerateAutoPlaylists(tracks);
    });
    QObject::connect(p->m_library, &MusicLibrary::tracksUpdated, this, [this](const TrackList& tracks) {
        p->handleTracksUpdated(tracks);
        p->regenerateAutoPlaylists(tracks);
    });

    p->m_settings->subscribe<Settings::Core::ShuffleAlbumsGroupScript>(this, [this]() { p->resetShuffleOrder(); });
    p->m_settings->subscribe<Settings::Core::ShuffleAlbumsSortScript>(this, [this]() { p->resetShuffleOrder(); });
}

PlaylistHandler::~PlaylistHandler()
{
    p->m_playlists.clear();
}

Playlist* PlaylistHandler::playlistById(const UId& id) const
{
    if(!id.isValid()) {
        return nullptr;
    }

    auto playlist = std::ranges::find_if(p->m_playlists, [id](const auto& pl) { return pl->id() == id; });
    if(playlist != p->m_playlists.cend()) {
        return playlist->get();
    }

    return nullptr;
}

Playlist* PlaylistHandler::playlistByDbId(int id) const
{
    if(id < 0) {
        return nullptr;
    }

    auto playlist = std::ranges::find_if(p->m_playlists, [id](const auto& pl) { return pl->dbId() == id; });
    if(playlist != p->m_playlists.cend()) {
        return playlist->get();
    }

    return nullptr;
}

Playlist* PlaylistHandler::playlistByIndex(int index) const
{
    if(index < 0) {
        return nullptr;
    }

    auto playlist = std::ranges::find_if(p->m_playlists, [index](const auto& pl) { return pl->index() == index; });
    if(playlist != p->m_playlists.cend()) {
        return playlist->get();
    }

    return nullptr;
}

Playlist* PlaylistHandler::playlistByName(const QString& name) const
{
    auto playlist = std::ranges::find_if(p->m_playlists, [name](const auto& pl) { return pl->name() == name; });
    if(playlist != p->m_playlists.cend()) {
        return playlist->get();
    }

    return nullptr;
}

PlaylistList PlaylistHandler::playlists() const
{
    PlaylistList playlists;

    for(const auto& playlist : p->m_playlists) {
        if(!playlist->isTemporary()) {
            playlists.emplace_back(playlist.get());
        }
    }

    return playlists;
}

PlaylistList PlaylistHandler::removedPlaylists() const
{
    PlaylistList playlists;

    for(const auto& playlist : p->m_removedPlaylists) {
        if(!playlist->isTemporary()) {
            playlists.emplace_back(playlist.get());
        }
    }

    return playlists;
}

PlaylistList PlaylistHandler::pendingRemovedPlaylists() const
{
    PlaylistList playlists;

    for(const auto& playlist : p->m_removedPlaylists) {
        if(playlist->isTemporary()) {
            continue;
        }

        const auto pendingIt = p->m_pendingRemovedPlaylists.find(removedPlaylistKey(playlist->name()));
        if(pendingIt != p->m_pendingRemovedPlaylists.cend() && pendingIt->second == playlist.get()) {
            playlists.emplace_back(playlist.get());
        }
    }

    return playlists;
}

Playlist* PlaylistHandler::createEmptyPlaylist()
{
    const QString name = p->findUniqueName(u"Playlist"_s);
    return createPlaylist(name);
}

Playlist* PlaylistHandler::createTempEmptyPlaylist()
{
    const QString name = p->findUniqueName(u"TempPlaylist"_s);
    return createTempPlaylist(name);
}

Playlist* PlaylistHandler::createPlaylist(const QString& name)
{
    const bool isNew = p->indexFromName(p->m_playlists, name) < 0;
    auto* playlist   = p->addNewPlaylist(name);

    if(playlist && isNew) {
        Q_EMIT playlistAdded(playlist);
    }

    return playlist;
}

Playlist* PlaylistHandler::createTempPlaylist(const QString& name)
{
    return p->addNewPlaylist(name, true);
}

Playlist* PlaylistHandler::createPlaylist(const QString& name, const TrackList& tracks)
{
    const bool isNew = p->indexFromName(p->m_playlists, name) < 0;
    auto* playlist   = p->addNewPlaylist(name);

    if(playlist) {
        p->replacePlaylistTracks(playlist, rebuildPlaylistTracks(playlist, tracks));
        if(isNew) {
            Q_EMIT playlistAdded(playlist);
        }
    }

    return playlist;
}

Playlist* PlaylistHandler::createTempPlaylist(const QString& name, const TrackList& tracks)
{
    auto* playlist = p->addNewPlaylist(name, true);
    if(playlist) {
        p->replacePlaylistTracks(playlist, rebuildPlaylistTracks(playlist, tracks));
    }

    return playlist;
}

Playlist* PlaylistHandler::createNewPlaylist(const QString& name)
{
    const QString newName = p->findUniqueName(name);
    auto* playlist        = p->addNewPlaylist(newName);

    if(playlist) {
        Q_EMIT playlistAdded(playlist);
    }

    return playlist;
}

Playlist* PlaylistHandler::createNewTempPlaylist(const QString& name)
{
    const QString newName = p->findUniqueName(name);
    return p->addNewPlaylist(newName, true);
}

Playlist* PlaylistHandler::createNewPlaylist(const QString& name, const TrackList& tracks)
{
    const QString newName = p->findUniqueName(name);
    auto* playlist        = p->addNewPlaylist(newName);

    if(playlist) {
        p->replacePlaylistTracks(playlist, rebuildPlaylistTracks(playlist, tracks));
        Q_EMIT playlistAdded(playlist);
    }

    return playlist;
}

Playlist* PlaylistHandler::createNewTempPlaylist(const QString& name, const TrackList& tracks)
{
    const QString newName = p->findUniqueName(name);
    auto* playlist        = p->addNewPlaylist(newName, true);
    if(playlist) {
        p->replacePlaylistTracks(playlist, rebuildPlaylistTracks(playlist, tracks));
    }

    return playlist;
}

Playlist* PlaylistHandler::createAutoPlaylist(const QString& name, const QString& query, const QString& sortQuery,
                                              bool forceSorted)
{
    const bool isNew = p->indexFromName(p->m_playlists, name) < 0;
    auto* playlist   = p->addNewAutoPlaylist(name, query, sortQuery, forceSorted);

    if(playlist) {
        const bool queryChanged       = playlist->query() != query;
        const bool sortQueryChanged   = playlist->sortQuery() != sortQuery;
        const bool forceSortedChanged = playlist->forceSorted() != forceSorted;
        if(isNew || queryChanged || sortQueryChanged || forceSortedChanged) {
            playlist->setQuery(query);
            playlist->setSortQuery(sortQuery);
            playlist->setForceSorted(forceSorted);

            const TrackList regeneratedTracks      = playlist->autoPlaylistTracks(p->m_library->tracks());
            const PlaylistTrackList playlistTracks = rebuildPlaylistTracks(playlist, regeneratedTracks);

            if(!playlistTracksSameData(playlist->playlistTracks(), PlaylistTrack::updateIndexes(playlistTracks))) {
                p->replacePlaylistTracks(playlist, playlistTracks);
            }
            else if(!isNew) {
                Q_EMIT playlistUpdated(playlist);
            }
        }
        if(isNew) {
            Q_EMIT playlistAdded(playlist);
        }
    }

    return playlist;
}

Playlist* PlaylistHandler::createNewAutoPlaylist(const QString& name, const QString& query, const QString& sortQuery,
                                                 bool forceSorted)
{
    const QString newName = p->findUniqueName(name);
    auto* playlist        = p->addNewAutoPlaylist(newName, query, sortQuery, forceSorted);

    if(playlist) {
        const TrackList regeneratedTracks = playlist->autoPlaylistTracks(p->m_library->tracks());
        p->replacePlaylistTracks(playlist, rebuildPlaylistTracks(playlist, regeneratedTracks));
        Q_EMIT playlistAdded(playlist);
    }

    return playlist;
}

void PlaylistHandler::appendToPlaylist(const UId& id, const TrackList& tracks)
{
    if(auto* playlist = playlistById(id)) {
        auto playlistTracks       = playlist->playlistTracks();
        const auto appendedTracks = PlaylistTrack::fromTracks(tracks, id);
        playlistTracks.insert(playlistTracks.end(), appendedTracks.cbegin(), appendedTracks.cend());
        p->replacePlaylistTracks(playlist, playlistTracks);
    }
}

void PlaylistHandler::replacePlaylistTracks(const UId& id, const TrackList& tracks, PlaylistTrackChangeSource source)
{
    if(auto* playlist = playlistById(id)) {
        p->replacePlaylistTracks(playlist, rebuildPlaylistTracks(playlist, tracks), source);
    }
}

void PlaylistHandler::replacePlaylistTracks(const UId& id, const PlaylistTrackList& tracks,
                                            PlaylistTrackChangeSource source)
{
    if(auto* playlist = playlistById(id)) {
        p->replacePlaylistTracks(playlist, tracks, source);
    }
}

void PlaylistHandler::movePlaylistTracks(const UId& id, const UId& replaceId)
{
    if(auto* playlist = playlistById(id)) {
        if(auto* replacePlaylist = playlistById(replaceId)) {
            createPlaylist(replacePlaylist->name(), playlist->tracks());
            replacePlaylist->changeCurrentIndex(playlist->currentTrackIndex());

            Q_EMIT playlistReferencesRemapRequested(id, replaceId);
            if(p->m_activePlaylist == playlist) {
                changeActivePlaylist(replacePlaylist);
            }
        }
    }
}

void PlaylistHandler::removePlaylistTracks(const UId& id, const std::vector<int>& indexes)
{
    if(auto* playlist = playlistById(id)) {
        auto playlistTracks = playlist->playlistTracks();
        std::set<int> indexesToRemove{indexes.cbegin(), indexes.cend()};
        for(const int index : indexesToRemove | std::views::reverse) {
            if(index >= 0 && std::cmp_less(index, playlistTracks.size())) {
                playlistTracks.erase(playlistTracks.begin() + index);
            }
        }
        p->replacePlaylistTracks(playlist, playlistTracks);
    }
}

void PlaylistHandler::clearPlaylistTracks(const UId& id)
{
    replacePlaylistTracks(id, TrackList{});
}

std::vector<int> PlaylistHandler::duplicateTrackIndexes(const UId& id) const
{
    const auto genHash = [](const Track& track) {
        QString title = track.title();
        if(title.isEmpty()) {
            title = track.directory() + track.filename();
        }

        return Utils::generateHash(track.artist(), track.album(), track.discNumber(), track.trackNumber(), title,
                                   QString::number(track.subsong()), QString::number(track.duration()));
    };

    if(auto* playlist = playlistById(id)) {
        const TrackList tracks = playlist->tracks();
        if(tracks.empty()) {
            return {};
        }

        std::unordered_set<QString> seen;
        std::vector<int> duplicateIndexes;

        const auto count = static_cast<int>(tracks.size());
        for(int i{0}; i < count; ++i) {
            const QString& key = genHash(tracks.at(i));
            if(!seen.insert(key).second) {
                duplicateIndexes.push_back(i);
            }
        }

        return duplicateIndexes;
    }

    return {};
}

std::vector<int> PlaylistHandler::deadTrackIndexes(const UId& id) const
{
    if(auto* playlist = playlistById(id)) {
        const TrackList tracks = playlist->tracks();
        if(tracks.empty()) {
            return {};
        }

        std::vector<int> deadIndexes;

        const auto count = static_cast<int>(tracks.size());
        for(int i{0}; i < count; ++i) {
            const Track& track = tracks.at(i);

            if(!track.isInArchive()) {
                if(!QFileInfo::exists(track.filepath())) {
                    deadIndexes.emplace_back(i);
                }
            }
            else {
                if(!QFileInfo::exists(track.archivePath())) {
                    deadIndexes.emplace_back(i);
                }
            }
        }

        return deadIndexes;
    }

    return {};
}

void PlaylistHandler::handleTracksDeleted(const TrackList& tracks)
{
    p->handleTracksDeleted(tracks);
    p->regenerateAutoPlaylists(tracks);
}

void PlaylistHandler::changePlaylistIndex(const UId& id, int index)
{
    if(auto* playlist = playlistById(id)) {
        if(!playlist->isTemporary() && index != playlist->index()) {
            Utils::move(p->m_playlists, playlist->index(), index);
            p->updateIndices();
            Q_EMIT playlistIndexChanged(playlist);
        }
    }
}

void PlaylistHandler::changeActivePlaylist(const UId& id)
{
    auto playlist
        = std::ranges::find_if(std::as_const(p->m_playlists), [id](const auto& pl) { return pl->id() == id; });
    if(playlist != p->m_playlists.cend()) {
        p->m_activePlaylist = playlist->get();
        Q_EMIT activePlaylistChanged(playlist->get());
    }
}

void PlaylistHandler::changeActivePlaylist(Playlist* playlist)
{
    p->m_activePlaylist = playlist;
    Q_EMIT activePlaylistChanged(playlist);
}

void PlaylistHandler::renamePlaylist(const UId& id, const QString& name)
{
    if(playlistCount() < 1) {
        return;
    }

    auto* playlist = playlistById(id);
    if(!playlist) {
        qCDebug(PL_HANDLER) << "Playlist could not be renamed to" << name;
        return;
    }

    const QString newName = p->findUniqueName(name.isEmpty() ? u"Playlist"_s : name);

    if(!playlist->isTemporary() && !p->m_playlistConnector.renamePlaylist(playlist->dbId(), newName)) {
        qCDebug(PL_HANDLER) << "Playlist could not be renamed to" << name;
        return;
    }

    playlist->setName(newName);
    p->cancelPendingRemovedPlaylist(newName);

    Q_EMIT playlistRenamed(playlist);
}

void PlaylistHandler::removePlaylist(const UId& id)
{
    auto* playlist = playlistById(id);
    if(!playlist) {
        return;
    }

    if(playlist->name() == "Default"_L1 && playlist->trackCount() == 0 && playlistCount() == 1
       && !playlist->isTemporary()) {
        return;
    }

    auto createDefaultIfEmpty = [this]() {
        if(p->noConcretePlaylists()) {
            createPlaylist(u"Default"_s, {});
        }
    };

    if(!playlist->isTemporary() && !p->m_playlistConnector.removePlaylist(playlist->dbId())) {
        createDefaultIfEmpty();
        return;
    }

    if(playlist == p->m_activePlaylist) {
        p->m_activePlaylist = nullptr;
        Q_EMIT activePlaylistChanged(nullptr);
        Q_EMIT activePlaylistDeleted();
    }

    const int index      = p->indexFromName(p->m_playlists, playlist->name());
    auto removedPlaylist = std::move(p->m_playlists.at(index));
    p->m_playlists.erase(p->m_playlists.begin() + index);
    p->m_removedPlaylists.emplace_back(std::move(removedPlaylist));
    p->trackPendingRemovedPlaylist(playlist);

    p->updateIndices();

    Q_EMIT playlistRemoved(playlist);

    createDefaultIfEmpty();
}

Playlist* PlaylistHandler::restorePlaylist(const UId& id)
{
    if(!id.isValid()) {
        return nullptr;
    }

    const auto playlistIt = std::ranges::find_if(p->m_removedPlaylists, [&id](const auto& playlist) {
        return playlist && !playlist->isTemporary() && playlist->id() == id;
    });
    if(playlistIt == p->m_removedPlaylists.cend()) {
        return nullptr;
    }

    auto* playlist = playlistIt->get();

    const QString oldName = playlist->name();
    const QString newName = p->findUniqueName(oldName.isEmpty() ? u"Playlist"_s : oldName);
    const int index       = p->nextValidIndex();
    const int dbId        = p->m_playlistConnector.insertPlaylist(
        newName, index, playlist->isAutoPlaylist(), playlist->query(), playlist->sortQuery(), playlist->forceSorted(),
        playlist->serialiseExtraProperties());

    if(dbId < 0) {
        return nullptr;
    }

    auto restoredPlaylist = std::move(*playlistIt);
    p->m_removedPlaylists.erase(playlistIt);

    playlist = restoredPlaylist.get();
    playlist->setName(newName);
    playlist->setDbId(dbId);
    playlist->setIndex(index);
    playlist->setTracksModified(true);

    p->cancelPendingRemovedPlaylist(oldName);
    p->m_playlists.emplace_back(std::move(restoredPlaylist));
    p->m_playlistConnector.savePlaylist(*playlist);

    Q_EMIT playlistAdded(playlist);

    return playlist;
}

Playlist* PlaylistHandler::activePlaylist() const
{
    return p->m_activePlaylist;
}

int PlaylistHandler::playlistCount() const
{
    return static_cast<int>(
        std::ranges::count_if(p->m_playlists, [](const auto& playlist) { return !playlist->isTemporary(); }));
}

PlaylistTrack PlaylistHandler::currentTrack() const
{
    return p->currentTrack();
}

PlaylistTrack PlaylistHandler::peekRelativeTrack(Playlist::PlayModes mode, int delta) const
{
    return p->nextTrack(delta, mode);
}

PlaylistTrack PlaylistHandler::advanceRelativeTrack(Playlist::PlayModes mode, int delta)
{
    return p->nextTrackChange(delta, mode);
}

void PlaylistHandler::savePlaylists()
{
    p->updateIndices();
    p->savePlaylists();
}

void PlaylistHandler::savePlaylist(const UId& id)
{
    if(auto* playlistToSave = playlistById(id)) {
        p->updateIndices();
        p->m_playlistConnector.savePlaylist(*playlistToSave);
    }
}

void PlaylistHandler::prepareUpcomingTrack()
{
    p->prepareUpcomingTrack();
}
} // namespace Fooyin

#include "core/playlist/moc_playlisthandler.cpp"
