/* LocalLibrary.cpp */

/* Copyright (C) 2011-2024 Michael Lugmair (Lucio Carreras)
 *
 * This file is part of sayonara player
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "LocalLibrary.h"

#include "Components/LibraryManagement/LibraryManager.h"
#include "Components/Tagging/ChangeNotifier.h"
#include "Database/Connector.h"
#include "Database/Library.h"
#include "Database/LibraryDatabase.h"
#include "Importer/LibraryImporter.h"
#include "Threads/ReloadThread.h"
#include "Threads/ReloadThreadFileScanner.h"
#include "Utils/Algorithm.h"
#include "Utils/ExtensionSet.h"
#include "Utils/FileSystem.h"
#include "Utils/FileUtils.h"
#include "Utils/Language/Language.h"
#include "Utils/Library/LibraryInfo.h"
#include "Utils/Logger/Logger.h"
#include "Utils/MetaData/Album.h"
#include "Utils/MetaData/Artist.h"
#include "Utils/MetaData/MetaDataList.h"
#include "Utils/MetaData/MetaDataSorting.h"
#include "Utils/Set.h"
#include "Utils/Settings/Settings.h"
#include "Utils/Tagging/TagReader.h"

namespace
{
	template<typename T>
	QHash<Id, int> createIdRowMap(const T& items)
	{
		auto result = QHash<Id, int>{};
		for (auto it = items.begin(); it != items.end(); ++it)
		{
			result[it->id()] = std::distance(items.begin(), it);
		}

		return result;
	}

	template<typename T>
	Util::Set<Id> collectIds(const IndexSet& indexes, const T& items)
	{
		auto result = Util::Set<Id>{};
		for (const auto& index: indexes)
		{
			if (Util::between(index, items.count()))
			{
				result.insert(items[index].id());
			}
		}

		return result;
	}

	template<typename T>
	IndexSet restoreSelectedIndexes(const T& items, const IdSet& oldSelections)
	{
		auto newSelections = IndexSet{};
		for (auto it = items.begin(); it != items.end(); ++it)
		{
			if (oldSelections.contains(it->id()))
			{
				newSelections.insert(std::distance(items.begin(), it));
			}
		}

		return newSelections;
	}

	Gui::ExtensionSet extractExtensions(const MetaDataList& tracks)
	{
		Gui::ExtensionSet extensions;
		for (const auto& track: tracks)
		{
			extensions.addExtension(Util::File::getFileExtension(track.filepath()), false);
		}

		return extensions;
	}

	QString createDeletionInfoString(const int trackCount, const int failCount, const Library::TrackDeletionMode mode)
	{
		const auto fileEntry = (mode == Library::TrackDeletionMode::AlsoFiles)
			                       ? Lang::get(Lang::Files)
			                       : Lang::get(Lang::Entries);

		return (failCount == 0)
			       ? QObject::tr("All %1 could be removed").arg(fileEntry)
			       : QObject::tr("%1 of %2 %3 could not be removed")
			         .arg(failCount)
			         .arg(trackCount)
			         .arg(fileEntry);
	}
}

using namespace Library;

struct LocalLibrary::Private
{
	LibraryId libraryId;
	DB::LibraryDatabase* libraryDatabase;

	Manager* libraryManager;
	ReloadThread* reloadThread = nullptr;
	Importer* libraryImporter = nullptr;

	LibraryPlaylistInteractor* playlistInteractor;
	Util::FileSystemPtr fileSystem;

	Util::Set<ArtistId> selectedArtists;
	Util::Set<AlbumId> selectedAlbums;
	Util::Set<TrackID> selectedTracks;

	ArtistList artists;
	AlbumList albums;
	MetaDataList tracks;
	MetaDataList selectedTrackList;
	MetaDataList filteredTrackList;

	Gui::ExtensionSet extensions;

	Filter filter;
	bool loaded{false};

	Private(const LibraryId libraryId, Manager* libraryManager, DB::LibraryDatabase* libraryDatabase,
	        LibraryPlaylistInteractor* playlistInteractor, Util::FileSystemPtr fileSystem) :
		libraryId{libraryId},
		libraryDatabase{libraryDatabase},
		libraryManager{libraryManager},
		playlistInteractor{playlistInteractor},
		fileSystem{std::move(fileSystem)}
	{
	}
};

LocalLibrary::LocalLibrary(Manager* libraryManager, const LibraryId libraryId, DB::LibraryDatabase* libraryDatabase,
                           LibraryPlaylistInteractor* playlistInteractor, const Util::FileSystemPtr& fileSystem,
                           QObject* parent) :
	QObject(parent),
	m{Pimpl::make<Private>(libraryId, libraryManager, libraryDatabase, playlistInteractor, fileSystem)}
{
	connect(libraryManager, &Manager::sigRenamed, this, [&](const auto id) {
		if (id == m->libraryId)
		{
			emit sigRenamed(info().name());
		}
	});

	connect(libraryManager, &Manager::sigPathChanged, this, [&](const auto id) {
		if (id == m->libraryId)
		{
			emit sigPathChanged(info().path());
		}
	});
}

LocalLibrary::~LocalLibrary() = default;

void LocalLibrary::init()
{
	if (!isLoaded())
	{
		initConnections();
		refetch();
	}

	m->loaded = true;
}

void LocalLibrary::initConnections()
{
	using Tagging::ChangeNotifier;

	ListenSettingNoCall(Set::Lib_SearchMode, LocalLibrary::searchModeChanged);
	ListenSettingNoCall(Set::Lib_ShowAlbumArtists, LocalLibrary::showAlbumArtistsChanged);
	ListenSettingNoCall(Set::Lib_SortModeMask, LocalLibrary::refreshCurrentView);

	auto* mdcn = ChangeNotifier::instance();
	connect(mdcn, &ChangeNotifier::sigMetadataChanged, this, &LocalLibrary::metadataChanged);
	connect(mdcn, &ChangeNotifier::sigMetadataDeleted, this, &LocalLibrary::metadataChanged);
	connect(mdcn, &ChangeNotifier::sigAlbumsChanged, this, &LocalLibrary::albumsChanged);
}

void LocalLibrary::metadataChanged()
{
	auto* mdcn = dynamic_cast<Tagging::ChangeNotifier*>(sender());

	const auto& changedTracks = mdcn->changedMetadata();
	const auto idRowMap = createIdRowMap(tracks());

	auto needsRefresh = false;
	for (const auto& [oldTrack, newTrack]: changedTracks)
	{
		needsRefresh = needsRefresh ||
		               (oldTrack.albumArtistId() != newTrack.albumArtistId()) ||
		               (oldTrack.albumId() != newTrack.albumId()) ||
		               (oldTrack.artistId() != newTrack.artistId()) ||
		               (oldTrack.album() != newTrack.album()) ||
		               (oldTrack.albumArtist() != newTrack.albumArtist()) ||
		               (oldTrack.artist() != newTrack.artist());

		if (idRowMap.contains(oldTrack.id()))
		{
			const auto row = idRowMap[oldTrack.id()];
			replaceTrack(row, newTrack);
		}
	}

	if (needsRefresh)
	{
		refreshCurrentView();
	}
}

void LocalLibrary::albumsChanged()
{
	auto* mdcn = dynamic_cast<Tagging::ChangeNotifier*>(sender());

	const auto& changedAlbums = mdcn->changedAlbums();
	const auto idRowMap = createIdRowMap(albums());

	for (const auto& [oldAlbum, newAlbum]: changedAlbums)
	{
		if (idRowMap.contains(oldAlbum.id()))
		{
			const auto row = idRowMap[oldAlbum.id()];
			replaceAlbum(row, newAlbum);
		}
	}
}

void LocalLibrary::reloadLibrary(const bool clearFirst, const ReloadQuality quality)
{
	if (isReloading())
	{
		return;
	}

	if (!m->reloadThread)
	{
		initReloadThread();
	}

	if (clearFirst)
	{
		deleteAllTracks();
	}

	const auto info = this->info();
	m->reloadThread->setLibrary(info.id(), info.path());
	m->reloadThread->setQuality(quality);
	m->reloadThread->start();
}

void LocalLibrary::reloadThreadFinished()
{
	refetch();

	emit sigReloadingLibrary(QString(), -1);
	emit sigReloadingLibraryFinished();
}

void LocalLibrary::searchModeChanged()
{
	spLog(Log::Debug, this) << "Updating cissearch... " << GetSetting(Set::Lib_SearchMode);

	m->libraryDatabase->updateSearchMode();

	spLog(Log::Debug, this) << "Updating cissearch finished" << GetSetting(Set::Lib_SearchMode);
}

void LocalLibrary::showAlbumArtistsChanged()
{
	refreshCurrentView();
}

void LocalLibrary::importStatusChanged(Importer::ImportStatus status)
{
	if (status == Importer::ImportStatus::Imported)
	{
		refreshCurrentView();
	}
}

ArtistList LocalLibrary::getAllArtists() const
{
	return m->libraryDatabase->getAllArtists(GetSetting(Set::Lib_ShowAlbumArtists), false);
}

ArtistList LocalLibrary::getAllArtistsBySearchstring(const Filter& filter) const
{
	return m->libraryDatabase->getAllArtistsBySearchString(GetSetting(Set::Lib_ShowAlbumArtists), filter);
}

AlbumList LocalLibrary::getAllAlbums() const { return m->libraryDatabase->getAllAlbums(false); }

AlbumList LocalLibrary::getAllAlbumsByArtist(const IdList& artistIds, const Filter& filter) const
{
	return m->libraryDatabase->getAllAlbumsByArtist(artistIds, filter);
}

AlbumList LocalLibrary::getAllAlbumsBySearchstring(const Filter& filter) const
{
	return m->libraryDatabase->getAllAlbumsBySearchString(filter);
}

int LocalLibrary::getTrackCount() const { return m->libraryDatabase->getNumTracks(); }

MetaDataList LocalLibrary::getAllTracks() const { return m->libraryDatabase->getAllTracks(); }

MetaDataList LocalLibrary::getAllTracks(const QStringList& paths) const
{
	return m->libraryDatabase->getMultipleTracksByPath(paths);
}

MetaDataList LocalLibrary::getAllTracksByArtist(const IdList& artistIds, const Filter& filter) const
{
	return m->libraryDatabase->getAllTracksByArtist(artistIds, filter);
}

MetaDataList LocalLibrary::getAllTracksByAlbum(const IdList& albumIds, const Filter& filter) const
{
	return m->libraryDatabase->getAllTracksByAlbum(albumIds, filter, -1);
}

MetaDataList LocalLibrary::getAllTracksBySearchstring(const Filter& filter) const
{
	return m->libraryDatabase->getAllTracksBySearchString(filter);
}

MetaDataList LocalLibrary::getAllTracksByPath(const QStringList& paths) const
{
	return m->libraryDatabase->getAllTracksByPaths(paths);
}

MetaData LocalLibrary::getTrackById(const TrackID trackId) const
{
	const auto tmpTrack = m->libraryDatabase->getTrackById(trackId);
	return (tmpTrack.libraryId() == m->libraryId)
		       ? tmpTrack
		       : MetaData();
}

Album LocalLibrary::getAlbumById(const AlbumId albumId) const { return m->libraryDatabase->getAlbumByID(albumId); }

Artist LocalLibrary::getArtistById(const ArtistId artistId) const
{
	return m->libraryDatabase->getArtistByID(artistId);
}

void LocalLibrary::initReloadThread()
{
	if (!m->reloadThread)
	{
		m->reloadThread =
			new ReloadThread(ReloadThreadFileScanner::create(), Tagging::TagReader::create(), this);

		connect(m->reloadThread, &ReloadThread::sigReloadingLibrary, this, &LocalLibrary::sigReloadingLibrary);
		connect(m->reloadThread, &ReloadThread::sigNewBlockSaved, this, &LocalLibrary::refreshCurrentView);
		connect(m->reloadThread, &ReloadThread::finished, this, &LocalLibrary::reloadThreadFinished);
	}
}

void LocalLibrary::deleteCurrentTracks(const TrackDeletionMode mode) { deleteTracks(currentTracks(), mode); }

void LocalLibrary::deleteFetchedTracks(const TrackDeletionMode mode) { deleteTracks(tracks(), mode); }

void LocalLibrary::deleteAllTracks() { deleteTracks(getAllTracks(), TrackDeletionMode::OnlyLibrary); }

void LocalLibrary::deleteTracks(const MetaDataList& tracks, const TrackDeletionMode mode)
{
	if (mode == TrackDeletionMode::None)
	{
		return;
	}

	m->libraryDatabase->deleteTracks(Util::trackIds(tracks));

	auto fails = 0;
	if (mode == TrackDeletionMode::AlsoFiles)
	{
		auto paths = QStringList{};
		Util::Algorithm::transform(tracks, paths, [](const auto& track) { return track.filepath(); });

		fails = m->fileSystem->deleteFiles(paths);
	}

	const auto answerString = createDeletionInfoString(tracks.count(), fails, mode);
	emit sigDeleteAnswer(answerString);

	Tagging::ChangeNotifier::instance()->deleteMetadata(tracks);
	refreshCurrentView();
}

void LocalLibrary::importFiles(const QStringList& files) { importFilesTo(files, {}); }

void LocalLibrary::importFilesTo(const QStringList& files, const QString& targetDirectory)
{
	if (!files.isEmpty())
	{
		importer()->import(info().path(), files, targetDirectory);
		emit sigImportDialogRequested(targetDirectory);
	}
}

bool LocalLibrary::setLibraryPath(const QString& libraryPath) const
{
	return m->libraryManager->changeLibraryPath(m->libraryId, libraryPath);
}

bool LocalLibrary::setLibraryName(const QString& libraryName) const
{
	return m->libraryManager->renameLibrary(m->libraryId, libraryName);
}

Info LocalLibrary::info() const
{
	return m->libraryManager->libraryInfo(m->libraryId);
}

Importer* LocalLibrary::importer()
{
	if (!m->libraryImporter)
	{
		m->libraryImporter = new Importer(m->libraryDatabase, Util::FileSystem::create(),
		                                  Tagging::TagReader::create(), this);

		connect(m->libraryImporter, &Importer::sigStatusChanged, this, &LocalLibrary::importStatusChanged);
	}

	return m->libraryImporter;
}

bool LocalLibrary::isReloading() const { return m->reloadThread && m->reloadThread->isRunning(); }

bool LocalLibrary::isLoaded() const { return m->loaded; }

void LocalLibrary::emitAll()
{
	prepareArtists();
	prepareAlbums();
	prepareTracks();

	emit sigAllArtistsLoaded();
	emit sigAllAlbumsLoaded();
	emit sigAllTracksLoaded();
}

void LocalLibrary::refetch()
{
	m->selectedArtists.clear();
	m->selectedAlbums.clear();
	m->selectedTracks.clear();
	m->filter.clear();

	m->artists = getAllArtists();
	m->albums = getAllAlbums();
	m->tracks = getAllTracks();

	emitAll();
}

void LocalLibrary::refreshCurrentView()
{
	/* Waring! Sorting after each fetch is important here! */
	/* Do not call emit_stuff() in order to avoid double sorting */
	const auto selectedArtists = std::move(m->selectedArtists);
	const auto selectedAlbums = std::move(m->selectedAlbums);
	const auto selectedTracks = std::move(m->selectedTracks);

	fetchByFilter(m->filter, true);

	prepareArtists();
	const auto selectedArtistIndexes = restoreSelectedIndexes(artists(), selectedArtists);
	changeArtistSelection(selectedArtistIndexes);

	prepareAlbums();
	const auto selectedAlbumIndexes = restoreSelectedIndexes(albums(), selectedAlbums);
	changeAlbumSelection(selectedAlbumIndexes);

	prepareTracks();
	const auto selectedTrackIndexes = restoreSelectedIndexes(tracks(), selectedTracks);

	emit sigAllAlbumsLoaded();
	emit sigAllArtistsLoaded();
	emit sigAllTracksLoaded();

	if (!selectedTrackIndexes.isEmpty())
	{
		changeTrackSelection(selectedTrackIndexes);
	}
}

void LocalLibrary::findTrack(const TrackID id)
{
	const auto track = getTrackById(id);
	if (track.id() < 0)
	{
		return;
	}

	if (!m->selectedArtists.isEmpty())
	{
		selectedArtistsChanged({});
	}

	if (!m->selectedAlbums.isEmpty())
	{
		selectedAlbumsChanged({});
	}

	// make sure, that no artist_selection_changed or album_selection_changed
	// messes things up
	emitAll();

	m->tracks.clear();
	m->artists.clear();
	m->albums.clear();

	m->selectedTracks.clear();
	m->filteredTrackList.clear();
	m->selectedArtists.clear();
	m->selectedAlbums.clear();

	auto artist = getArtistById(track.artistId());
	m->artists.push_back(std::move(artist));

	auto album = getAlbumById(track.albumId());
	m->albums.push_back(std::move(album));

	m->tracks = getAllTracksByAlbum({track.albumId()}, {});
	m->selectedTracks << track.id();

	emitAll();
}

void LocalLibrary::changeArtistSelection(const IndexSet& indexes)
{
	auto selectedArtists = collectIds(indexes, m->artists);
	if (selectedArtists == m->selectedArtists)
	{
		return;
	}

	m->selectedArtists = std::move(selectedArtists);

	if (!m->selectedArtists.isEmpty() && (m->selectedArtists.size() < m->artists.size()))
	{
		m->tracks = getAllTracksByArtist(m->selectedArtists.toList(), m->filter);
		m->albums = getAllAlbumsByArtist(m->selectedArtists.toList(), m->filter);
	}

	else if (!m->filter.cleared())
	{
		m->tracks = getAllTracksBySearchstring(m->filter);
		m->albums = getAllAlbumsBySearchstring(m->filter);
		m->artists = getAllArtistsBySearchstring(m->filter);
	}

	else
	{
		m->tracks = getAllTracks();
		m->albums = getAllAlbums();
	}

	prepareArtists();
	prepareAlbums();
	prepareTracks();
}

const AlbumList& LocalLibrary::albums() const { return m->albums; }

const ArtistList& LocalLibrary::artists() const { return m->artists; }

const MetaDataList& LocalLibrary::currentTracks() const
{
	return m->selectedTracks.isEmpty() ? tracks() : m->selectedTrackList;
}

const MetaDataList& LocalLibrary::tracks() const
{
	return m->filteredTrackList.isEmpty() ? m->tracks : m->filteredTrackList;
}

void LocalLibrary::changeCurrentDisc(const Disc disc)
{
	if (m->selectedAlbums.size() != 1)
	{
		return;
	}

	m->tracks = getAllTracksByAlbum(m->selectedAlbums.toList(), m->filter);

	if (disc != std::numeric_limits<Disc>::max())
	{
		m->tracks.removeTracks([disc](const MetaData& track) {
			return (track.discnumber() != disc);
		});
	}

	prepareTracks();
	emit sigAllTracksLoaded();
}

const IdSet& LocalLibrary::selectedAlbums() const { return m->selectedAlbums; }

const IdSet& LocalLibrary::selectedArtists() const { return m->selectedArtists; }

Filter LocalLibrary::filter() const { return m->filter; }

void LocalLibrary::changeFilter(Filter filter, const bool force)
{
	if (filter.mode() != Filter::InvalidGenre)
	{
		const auto filtertext = filter.filtertext(false);
		const auto searchStringLength = GetSetting(Set::Lib_SearchStringLength);

		if (filtertext.join("").size() < searchStringLength)
		{
			filter.clear();
		}

		else
		{
			filter.setFiltertext(filtertext.join(","));
		}
	}

	if (!filter.isEqual(m->filter, GetSetting(Set::Lib_SearchStringLength)))
	{
		fetchByFilter(filter, force);
		emitAll();
	}
}

void LocalLibrary::selectedArtistsChanged(const IndexSet& indexes)
{
	// happens, when the model is set at initialization of table views
	if (m->selectedArtists.isEmpty() && indexes.isEmpty())
	{
		return;
	}

	changeArtistSelection(indexes);

	emit sigAllAlbumsLoaded();
	emit sigAllTracksLoaded();
}

void LocalLibrary::changeAlbumSelection(const IndexSet& indexes, bool ignoreArtists)
{
	m->tracks.clear();
	m->selectedAlbums = collectIds(indexes, m->albums);

	// only show tracks of selected album / artist
	if (!m->selectedArtists.isEmpty() && !ignoreArtists)
	{
		if (!m->selectedAlbums.isEmpty())
		{
			const auto tracks = getAllTracksByAlbum(m->selectedAlbums.toList(), m->filter);

			Util::Algorithm::moveIf(tracks, m->tracks, [&](const auto& track) {
				const auto artistId = GetSetting(Set::Lib_ShowAlbumArtists)
					                      ? track.albumArtistId()
					                      : track.artistId();

				return m->selectedArtists.contains(artistId);
			});
		}

		else
		{
			m->tracks = getAllTracksByArtist(m->selectedArtists.toList(), m->filter);
		}
	}

	else if (!m->selectedAlbums.isEmpty())
	{
		m->tracks = getAllTracksByAlbum(m->selectedAlbums.toList(), m->filter);
	}

	else if (!m->filter.cleared())
	{
		m->tracks = getAllTracksBySearchstring(m->filter);
	}

	else
	{
		m->tracks = getAllTracks();
	}

	prepareTracks();
}

void LocalLibrary::selectedAlbumsChanged(const IndexSet& indexes, const bool ignoreArtists)
{
	// happens, when the model is set at initialization of table views
	if (m->selectedAlbums.isEmpty() && indexes.isEmpty())
	{
		return;
	}

	changeAlbumSelection(indexes, ignoreArtists);
	emit sigAllTracksLoaded();
}

void LocalLibrary::changeTrackSelection(const IndexSet& indexes)
{
	m->selectedTrackList.clear();
	m->selectedTracks.clear();
	for (const auto& index: indexes)
	{
		if (Util::between(index, m->tracks))
		{
			m->selectedTrackList << m->tracks[index];
			m->selectedTracks.insert(m->tracks[index].id());
		}
	}
}

void LocalLibrary::selectedTracksChanged(const IndexSet& indexes)
{
	changeTrackSelection(indexes);
}

void LocalLibrary::fetchByFilter(const Filter& filter, bool force)
{
	if (m->filter.isEqual(filter, GetSetting(Set::Lib_SearchStringLength)) &&
	    m->selectedArtists.empty() &&
	    m->selectedAlbums.empty() &&
	    !force)
	{
		return;
	}

	m->filter = filter;

	m->selectedArtists.clear();
	m->selectedAlbums.clear();

	if (m->filter.cleared())
	{
		m->artists = getAllArtists();
		m->albums = getAllAlbums();
		m->tracks = getAllTracks();

		return;
	}

	m->artists = getAllArtistsBySearchstring(m->filter);
	m->albums = getAllAlbumsBySearchstring(m->filter);
	m->tracks = getAllTracksBySearchstring(m->filter);
}

void LocalLibrary::fetchTracksByPath(const QStringList& paths)
{
	m->tracks = paths.isEmpty()
		            ? MetaDataList{}
		            : getAllTracksByPath(paths);

	emitAll();
}

void LocalLibrary::changeTrackSortorder(const TrackSortorder sortOrder)
{
	if (auto sorting = sortorder(); sortOrder != sorting.tracks)
	{
		sorting.tracks = sortOrder;
		SetSetting(Set::Lib_Sorting, sorting);

		prepareTracks();
		emit sigAllTracksLoaded();
	}
}

void LocalLibrary::changeAlbumSortorder(const AlbumSortorder sortOrder)
{
	if (auto sorting = sortorder(); sortOrder != sorting.album)
	{
		sorting.album = sortOrder;
		SetSetting(Set::Lib_Sorting, sorting);

		prepareAlbums();
		emit sigAllAlbumsLoaded();
	}
}

void LocalLibrary::changeArtistSortorder(const ArtistSortorder sortOrder)
{
	if (auto sorting = sortorder(); sortOrder != sorting.artist)
	{
		sorting.artist = sortOrder;
		SetSetting(Set::Lib_Sorting, sorting);

		prepareArtists();
		emit sigAllArtistsLoaded();
	}
}

Sortings LocalLibrary::sortorder() const { return GetSetting(Set::Lib_Sorting); }

void LocalLibrary::replaceAlbum(const int index, const Album& album)
{
	if (Util::between(index, m->albums))
	{
		m->albums[index] = album;
		emit sigCurrentAlbumChanged(index);
	}
}

void LocalLibrary::replaceTrack(const int index, const MetaData& track)
{
	if (Util::between(index, m->tracks))
	{
		m->tracks[index] = track;
		emit sigCurrentTrackChanged(index);
	}
}

void LocalLibrary::prepareTracks()
{
	m->filteredTrackList.clear();
	m->extensions = extractExtensions(tracks());

	MetaDataSorting::sortMetadata(m->tracks, sortorder().tracks, GetSetting(Set::Lib_SortModeMask));
}

void LocalLibrary::prepareAlbums()
{
	MetaDataSorting::sortAlbums(m->albums, sortorder().album, GetSetting(Set::Lib_SortModeMask));
}

void LocalLibrary::prepareArtists()
{
	MetaDataSorting::sortArtists(m->artists, sortorder().artist, GetSetting(Set::Lib_SortModeMask));
}

Gui::ExtensionSet LocalLibrary::extensions() const { return m->extensions; }

bool LocalLibrary::isEmpty() const { return m->tracks.isEmpty() && (getTrackCount() == 0); }

void LocalLibrary::setExtensions(const Gui::ExtensionSet& extensions)
{
	m->extensions = extensions;
	m->filteredTrackList.clear();

	if (m->extensions.hasEnabledExtensions())
	{
		Util::Algorithm::copyIf(m->tracks, m->filteredTrackList, [&](const auto& track) {
			const auto extension = ::Util::File::getFileExtension(track.filepath());
			return (m->extensions.isEnabled(extension));
		});
	}

	emit sigAllTracksLoaded();
}

LibraryPlaylistInteractor* LocalLibrary::playlistInteractor() const { return m->playlistInteractor; }
