/* DatabaseTracks.cpp */

/* Copyright (C) 2011-2024 Michael Lugmair (Lucio Carreras)
 *
 * module() file is part of sayonara player
 *
 * module() 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.

 * module() 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 module() program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "Database/Tracks.h"
#include "Database/Library.h"
#include "Database/Query.h"
#include "Database/Utils.h"

#include "Utils/MetaData/MetaDataList.h"
#include "Utils/MetaData/Genre.h"

#include "Utils/Algorithm.h"
#include "Utils/Utils.h"
#include "Utils/FileUtils.h"
#include "Utils/Set.h"
#include "Utils/Settings/Settings.h"
#include "Utils/Library/Filter.h"
#include "Utils/Library/SearchMode.h"
#include "Utils/Logger/Logger.h"

#include <QDateTime>
#include <QMap>

#include <utility>

using DB::Tracks;
using ::Library::Filter;

namespace
{
	constexpr const auto* CisPlaceholder = ":cissearch";

	enum class Column : int
	{
		Id = 0,
		Title,
		Duration,
		Year,
		Bitrate,
		Filepath,
		Filesize,
		TrackNumber,
		Genre,
		DiscNumber,
		Rating,
		AlbumId,
		ArtistId,
		AlbumArtistId,
		Comment,
		CreatedDate,
		ModifiedDate,
		LibraryId,
		AlbumName,
		AlbumRating,
		ArtistName,
		AlbumArtistName,
		AllCissearch,
		FileCissearch,
		GenreCissearch
	};

	constexpr int operator+(const Column c) { return static_cast<int>(c); }

	const auto basicFields = QMap<Column, QString>
	{
		{Column::Id, QStringLiteral("tracks.trackId AS trackId")},
		{Column::Title, QStringLiteral("tracks.title AS title")},
		{Column::Duration, QStringLiteral("tracks.length AS length")},
		{Column::Year, QStringLiteral("tracks.year AS year")},
		{Column::Bitrate, QStringLiteral("tracks.bitrate AS bitrate")},
		{Column::Filepath, QStringLiteral("tracks.filename AS filename")},
		{Column::Filesize, QStringLiteral("tracks.filesize AS filesize")},
		{Column::TrackNumber, QStringLiteral("tracks.track AS trackNum")},
		{Column::Genre, QStringLiteral("tracks.genre AS genre")},
		{Column::DiscNumber, QStringLiteral("tracks.discnumber AS discnumber")},
		{Column::Rating, QStringLiteral("tracks.rating AS rating")},
		{Column::AlbumId, QStringLiteral("tracks.albumId AS albumId")},
		{Column::ArtistId, QStringLiteral("tracks.artistId AS artistId")},
		{Column::AlbumArtistId, QStringLiteral("tracks.albumArtistId AS albumArtistId")},
		{Column::Comment, QStringLiteral("tracks.comment AS comment")},
		{Column::CreatedDate, QStringLiteral("tracks.createDate AS createDate")},
		{Column::ModifiedDate, QStringLiteral("tracks.modifyDate AS modifyDate")},
		{Column::LibraryId, QStringLiteral("tracks.libraryId AS trackLibraryId")}
	};

	const auto searchViewFields = QMap<Column, QString>
	{
		{Column::AlbumRating, QStringLiteral("albums.rating AS albumRating")},
		{Column::AlbumName, QStringLiteral("albums.name AS albumName")},
		{Column::ArtistName, QStringLiteral("artists.name AS artistName")},
		{Column::AlbumArtistName, QStringLiteral("albumArtists.name AS albumArtistName")},
		{
			Column::AllCissearch,
			QStringLiteral("(albums.cissearch || ',' || artists.cissearch || ',' || tracks.cissearch) AS allCissearch")
		},
		{Column::FileCissearch, QStringLiteral("tracks.fileCissearch AS fileCissearch")},
		{Column::GenreCissearch, QStringLiteral("tracks.genreCissearch AS genreCissearch")}
	};

	QStringList collectBasicViewFields() { return {basicFields.values()}; }

	QStringList collectSearchViewFields() { return {basicFields.values() + searchViewFields.values()}; }

	void dropTrackView(DB::Module* module, LibraryId libraryId, const QString& trackView)
	{
		if (libraryId >= 0)
		{
			module->runQuery(QStringLiteral("DROP VIEW IF EXISTS %1;").arg(trackView),
			                 QStringLiteral("Cannot drop %1").arg(trackView));
		}
	}

	void dropTrackSearchView(DB::Module* module, const QString& trackSearchView)
	{
		module->runQuery(QStringLiteral("DROP VIEW IF EXISTS %1;").arg(trackSearchView),
		                 QStringLiteral("Cannot drop %1").arg(trackSearchView));
	}

	void createTrackView(DB::Module* module, const LibraryId libraryId, const QString& trackView)
	{
		if (libraryId >= 0)
		{
			const auto basicViewFields = collectBasicViewFields().join(", ");
			const auto query = QStringLiteral("CREATE VIEW %1 AS SELECT %2 FROM tracks WHERE tracks.libraryID = %3")
			                   .arg(trackView)
			                   .arg(basicViewFields)
			                   .arg(QString::number(libraryId));

			module->runQuery(query, "Cannot create track view");
		}
	}

	void createTrackSearchView(DB::Module* module, const LibraryId libraryId, const QString& trackSearchView)
	{
		const auto searchviewFields = collectSearchViewFields().join(", ");;
		const auto joinStatement = QStringLiteral("LEFT OUTER JOIN albums ON tracks.albumID = albums.albumID "
			"LEFT OUTER JOIN artists ON tracks.artistID = artists.artistID "
			"LEFT OUTER JOIN artists albumArtists ON tracks.albumArtistID = albumArtists.artistID");

		const auto whereStatement = (libraryId >= 0)
			                            ? QStringLiteral("libraryID=%1").arg(QString::number(libraryId))
			                            : QStringLiteral("1");

		auto query = QStringLiteral("CREATE VIEW %1 AS SELECT %2 FROM tracks %3 WHERE %4;")
		             .arg(trackSearchView)
		             .arg(searchviewFields)
		             .arg(joinStatement)
		             .arg(whereStatement);

		module->runQuery(query, QStringLiteral("Cannot create track search view"));
	}

	QMap<QString, QVariant>
	getTrackBindings(const MetaData& track, ArtistId artistId, AlbumId albumId, ArtistId albumArtistId)
	{
		return QMap<QString, QVariant>{
			{QStringLiteral("albumArtistID"), albumArtistId},
			{QStringLiteral("albumID"), albumId},
			{QStringLiteral("artistID"), artistId},
			{QStringLiteral("bitrate"), track.bitrate()},
			{QStringLiteral("cissearch"), Library::convertSearchstring(track.title())},
			{QStringLiteral("comment"), Util::convertNotNull(track.comment())},
			{QStringLiteral("discnumber"), track.discnumber()},
			{QStringLiteral("filecissearch"), Library::convertSearchstring(track.filepath())},
			{QStringLiteral("filename"), Util::File::cleanFilename(track.filepath())},
			{QStringLiteral("filesize"), QVariant::fromValue(track.filesize())},
			{QStringLiteral("genre"), Util::convertNotNull(track.genresToString())},
			{QStringLiteral("genreCissearch"), Library::convertSearchstring(track.genresToString())},
			{QStringLiteral("length"), QVariant::fromValue(track.durationMs())},
			{QStringLiteral("libraryID"), track.libraryId()},
			{QStringLiteral("rating"), QVariant(static_cast<int>(track.rating()))},
			{QStringLiteral("title"), Util::convertNotNull(track.title())},
			{QStringLiteral("track"), track.trackNumber()},
			{QStringLiteral("year"), track.year()}
		};
	}
} // namespace

Tracks::Tracks() = default;
Tracks::~Tracks() = default;

void Tracks::initViews()
{
	dropTrackView(module(), libraryId(), trackView());
	createTrackView(module(), libraryId(), trackView());

	dropTrackSearchView(module(), trackSearchView());
	createTrackSearchView(module(), libraryId(), trackSearchView());
}

QString Tracks::fetchQueryTracks(const QString& where) const
{
	return QStringLiteral("SELECT * FROM %1 WHERE %2;")
	       .arg(trackSearchView())
	       .arg(where.isEmpty() ? QStringLiteral("1") : where);
}

MetaDataList Tracks::dbFetchTracks(QSqlQuery& q) const
{
	if (!q.exec())
	{
		showError(q, QStringLiteral("Cannot fetch tracks from database"));
		return {};
	}

	auto result = MetaDataList{};

	while (q.next())
	{
		MetaData track;

		track.setId(q.value(+Column::Id).toInt());
		track.setTitle(q.value(+Column::Title).toString());
		track.setDurationMs(q.value(+Column::Duration).toInt());
		track.setYear(q.value(+Column::Year).value<Year>());
		track.setBitrate(q.value(+Column::Bitrate).value<Bitrate>());
		track.setFilepath(q.value(+Column::Filepath).toString());
		track.setFilesize(q.value(+Column::Filesize).value<Filesize>());
		track.setTrackNumber(q.value(+Column::TrackNumber).value<TrackNum>());
		track.setGenres(q.value(+Column::Genre).toString().split(","));
		track.setDiscnumber(q.value(+Column::DiscNumber).value<Disc>());
		track.setRating(q.value(+Column::Rating).value<Rating>());
		track.setAlbumId(q.value(+Column::AlbumId).toInt());
		track.setArtistId(q.value(+Column::ArtistId).toInt());
		track.setComment(q.value(+Column::Comment).toString());
		track.setCreatedDate(q.value(+Column::CreatedDate).value<uint64_t>());
		track.setModifiedDate(q.value(+Column::ModifiedDate).value<uint64_t>());
		track.setLibraryid(q.value(+Column::LibraryId).value<LibraryId>());
		track.setAlbum(q.value(+Column::AlbumName).toString().trimmed());
		track.setArtist(q.value(+Column::ArtistName).toString().trimmed());
		track.setAlbumArtist(q.value(+Column::AlbumArtistName).toString(), q.value(+Column::AlbumArtistId).toInt());

		result.push_back(std::move(track));
	}

	return result;
}

MetaDataList Tracks::getMultipleTracksByPath(const QStringList& paths) const
{
	auto result = MetaDataList{};
	for (const auto& path: paths)
	{
		auto track = getTrackByPath(path);
		if (track.id() >= 0)
		{
			result.push_back(std::move(track));
		}
	}

	return result;
}

MetaData Tracks::getSingleTrack(const QString& queryText, const std::pair<QString, QVariant>& binding,
                                const QString& errorMessage) const
{
	auto q = QSqlQuery(module()->db());
	q.prepare(queryText);
	q.bindValue(binding.first, binding.second);

	const auto tracks = dbFetchTracks(q);
	if (tracks.isEmpty())
	{
		spLog(Log::Warning, this) << errorMessage;
	}

	return tracks.isEmpty() ? MetaData{} : tracks[0];
}

MetaData Tracks::getTrackByPath(const QString& path) const
{
	const auto query = fetchQueryTracks(QStringLiteral("filename = :filename"));
	const auto cleanedPath = Util::File::cleanFilename(path);
	return getSingleTrack(query, {QStringLiteral(":filename"), cleanedPath},
	                      QStringLiteral("Cannot fetch track by path"));
}

MetaData Tracks::getTrackById(TrackID id) const
{
	const auto query = fetchQueryTracks(QStringLiteral("trackID = :trackId"));
	return getSingleTrack(query, {QStringLiteral(":trackId"), id}, QStringLiteral("Cannot fetch track by id"));
}

int Tracks::getNumTracks() const
{
	const auto query = QStringLiteral("SELECT COUNT(tracks.trackid) FROM tracks WHERE libraryID=:libraryID;");
	auto q = module()->runQuery(
		query,
		{QStringLiteral(":libraryID"), libraryId()},
		QStringLiteral("Cannot count tracks")
	);

	return (!hasError(q) && q.next())
		       ? q.value(0).toInt()
		       : -1;
}

MetaDataList Tracks::getAllTracksByIdList(const IdList& ids, const Filter& filter,
                                          std::function<bool(const MetaData&, Id)>&& trackMatchesId) const
{
	if (ids.isEmpty())
	{
		return {};
	}

	auto result = getAllTracks(filter);
	result.removeTracks([&](const auto& track) {
		return !Util::Algorithm::contains(ids, [&](const auto& id) {
			return trackMatchesId(track, id);;
		});
	});

	return result;
}

MetaDataList Tracks::getAllTracks() const
{
	auto q = QSqlQuery(module()->db());
	const auto query = fetchQueryTracks("");
	q.prepare(query);

	return dbFetchTracks(q);
}

MetaDataList Tracks::getAllTracks(const Filter& filter) const
{
	auto result = MetaDataList{};
	const auto whereStatement = filter.cleared()
		                            ? QString()
		                            : getFilterWhereStatement(filter, CisPlaceholder);

	const auto query = fetchQueryTracks(whereStatement);

	const auto searchFilters = filter.searchModeFiltertext(true, GetSetting(Set::Lib_SearchMode));
	for (const auto& searchFilter: searchFilters)
	{
		auto q = QSqlQuery(module()->db());
		q.prepare(query);
		q.bindValue(CisPlaceholder, searchFilter);

		auto tmpList = dbFetchTracks(q);
		if (result.isEmpty())
		{
			result = std::move(tmpList);
		}
		else
		{
			result.appendUnique(tmpList);
		}
	}

	return result;
}

MetaDataList Tracks::getAllTracksByAlbum(const IdList& albumsIds) const
{
	return getAllTracksByAlbum(albumsIds, Filter(), -1);
}

MetaDataList
Tracks::getAllTracksByAlbum(const IdList& albumIds, const Filter& filter, int discnumber) const
{
	const auto albumIdField = QStringLiteral("%1.albumID").arg(trackSearchView());

	auto result = getAllTracksByIdList(albumIds, filter, [](const auto& track, const auto id) {
		return track.albumId() == id;
	});

	if (discnumber >= 0)
	{
		result.removeTracks([&](const auto& track) {
			return (track.discnumber() != discnumber);
		});
	}

	return result;
}

MetaDataList Tracks::getAllTracksByArtist(const IdList& artistIds) const
{
	return getAllTracksByArtist(artistIds, {});
}

MetaDataList Tracks::getAllTracksByArtist(const IdList& artistIds, const Filter& filter) const
{
	return getAllTracksByIdList(artistIds, filter, [](const auto& track, const auto id) {
		return (track.artistId() == id) or (track.albumArtistId() == id);
	});
}

MetaDataList Tracks::getAllTracksBySearchString(const Filter& filter) const
{
	auto result = MetaDataList{};

	const auto whereStatement = getFilterWhereStatement(filter, CisPlaceholder);
	const auto query = fetchQueryTracks(whereStatement);

	const auto searchFilters = filter.searchModeFiltertext(true, GetSetting(Set::Lib_SearchMode));
	for (const auto& searchFilter: searchFilters)
	{
		auto q = QSqlQuery(module()->db());
		q.prepare(query);
		q.bindValue(CisPlaceholder, searchFilter);

		auto tracks = dbFetchTracks(q);
		if (result.isEmpty())
		{
			result = std::move(tracks);
		}
		else
		{
			result.appendUnique(tracks);
		}
	}

	return result;
}

MetaDataList Tracks::getAllTracksByPaths(const QStringList& paths) const
{
	QStringList queries;
	QMap<QString, QString> placeholderPathMapping;

	for (auto i = 0; i < paths.size(); i++)
	{
		const auto placeholder = QStringLiteral(":path%1").arg(i);
		const auto whereStatement = QStringLiteral("filename LIKE %1").arg(placeholder);
		auto query = fetchQueryTracks(whereStatement);
		query.remove(query.size() - 1, 1);

		queries << query;

		placeholderPathMapping[placeholder] = paths[i];
	}

	const auto query = queries.join(QStringLiteral(" UNION ")) + ';';
	auto q = QSqlQuery(module()->db());
	q.prepare(query);
	for (auto it = placeholderPathMapping.begin(); it != placeholderPathMapping.end(); ++it)
	{
		q.bindValue(it.key(), it.value() + '%');
	}

	return dbFetchTracks(q);
}

bool Tracks::deleteTrack(TrackID id)
{
	const auto queryText = QStringLiteral("DELETE FROM tracks WHERE trackID = :trackID;");
	const auto q = module()->runQuery(
		queryText,
		{QStringLiteral(":trackID"), id},
		QStringLiteral("Cannot delete track %1").arg(id));

	return !hasError(q);
}

bool Tracks::deleteTracks(const IdList& ids)
{
	if (ids.isEmpty())
	{
		return true;
	}

	module()->db().transaction();

	const auto fileCount = Util::Algorithm::count(ids, [this](const auto& id) {
		return deleteTrack(id);
	});

	const auto success = module()->db().commit();

	return (success && (fileCount == ids.size()));
}

MetaDataList Tracks::deleteInvalidTracks(const QString& libraryPath)
{
	auto doubleMetadata = MetaDataList{};

	const auto tracks = getAllTracks();
	if (!tracks.isEmpty())
	{
		spLog(Log::Error, this) << "Cannot get tracks from db";
		return {};
	}

	QMap<QString, int> trackIndexMap;
	Util::Set<Id> toDelete;
	int index = 0;

	for (const auto& track: tracks)
	{
		if (trackIndexMap.contains(track.filepath()))
		{
			spLog(Log::Warning, this) << "found double path: " << track.filepath();
			const auto trackIndex = trackIndexMap[track.filepath()];
			const auto& knownTrack = tracks[trackIndex];

			toDelete << track.id() << knownTrack.id();
			const auto contains = Util::Algorithm::contains(doubleMetadata, [&](const auto& doubleTrack) {
				return (knownTrack.id() == doubleTrack.id());
			});
			if (!contains)
			{
				doubleMetadata << knownTrack;
			}
		}

		else
		{
			trackIndexMap.insert(track.filepath(), index);
		}

		if (!libraryPath.isEmpty() && !track.filepath().contains(libraryPath))
		{
			toDelete << track.id();
		}

		index++;
	}

	spLog(Log::Debug, this) << "Deleting " << toDelete.size() << " tracks";
	auto success = deleteTracks(toDelete.toList());
	spLog(Log::Debug, this) << "delete tracks: " << success;

	return doubleMetadata;
}

Util::Set<Genre> Tracks::getAllGenres() const
{
	const auto query = QStringLiteral("SELECT genre FROM %1 GROUP BY genre;").arg(trackView());
	auto q = module()->runQuery(query, QStringLiteral("Cannot fetch genres"));
	if (hasError(q))
	{
		return {};
	}

	Util::Set<Genre> genres;
	while (q.next())
	{
		const auto genre = q.value(0).toString();
		const auto subgenres = genre.split(",");

		for (const auto& subgenre: subgenres)
		{
			genres.insert(Genre(subgenre));
		}
	}

	return genres;
}

void Tracks::updateTrackCissearch()
{
	const auto tracks = getAllTracks();

	module()->db().transaction();

	for (const auto& track: tracks)
	{
		module()->update(
			QStringLiteral("tracks"),
			{
				{QStringLiteral("cissearch"), ::Library::convertSearchstring(track.title())},
				{QStringLiteral("fileCissearch"), ::Library::convertSearchstring(track.filepath())},
				{QStringLiteral("genreCissearch"), ::Library::convertSearchstring(track.genresToString())}
			},
			{QStringLiteral("trackId"), track.id()},
			QStringLiteral("Cannot update album cissearch"));
	}

	module()->db().commit();
}

void Tracks::deleteAllTracks(const bool alsoViews)
{
	if (libraryId() >= 0)
	{
		if (alsoViews)
		{
			dropTrackView(module(), libraryId(), trackView());
			dropTrackSearchView(module(), trackSearchView());
		}

		const auto query = QStringLiteral("DELETE FROM tracks WHERE libraryId=:libraryId;");
		module()->runQuery(
			query,
			{QStringLiteral(":libraryId"), libraryId()},
			QStringLiteral("Cannot delete library tracks"));
	}
}

bool Tracks::updateTrack(const MetaData& track)
{
	if (track.id() < 0 || track.albumId() < 0 || track.artistId() < 0 || track.libraryId() < 0)
	{
		spLog(Log::Warning, this) << "Cannot update track (value negative): "
			<< " ArtistID: " << track.artistId()
			<< " AlbumID: " << track.albumId()
			<< " TrackID: " << track.id()
			<< " LibraryID: " << track.libraryId();
		return false;
	}

	auto bindings = getTrackBindings(track, track.artistId(), track.albumId(), track.albumArtistId());
	bindings["modifydate"] = QVariant::fromValue(Util::currentDateToInt());

	const auto q = module()->update(
		QStringLiteral("tracks"),
		bindings,
		{QStringLiteral("trackId"), track.id()},
		QStringLiteral("Cannot update track %1").arg(track.filepath()));

	return wasUpdateSuccessful(q);
}

bool Tracks::renameFilepaths(const QMap<QString, QString>& paths, LibraryId targetLibrary)
{
	module()->db().transaction();

	const auto originalPaths = paths.keys();
	for (const auto& originalPath: originalPaths)
	{
		const auto tracks = getAllTracksByPaths({originalPath});

		const auto newPath = paths[originalPath];
		for (const auto& track: tracks)
		{
			const auto oldFilepath = track.filepath();
			auto newFilepath = oldFilepath;
			newFilepath.replace(originalPath, newPath);

			renameFilepath(oldFilepath, newFilepath, targetLibrary);
		}
	}

	return module()->db().commit();
}

bool Tracks::renameFilepath(const QString& oldPath, const QString& newPath, LibraryId targetLibrary)
{
	const auto q = module()->update(
		QStringLiteral("tracks"),
		{
			{QStringLiteral("filename"), newPath},
			{QStringLiteral("libraryID"), targetLibrary}
		},

		{QStringLiteral("filename"), oldPath},
		QStringLiteral("Could not rename Filepath"));

	return wasUpdateSuccessful(q);
}

bool Tracks::insertTrackIntoDatabase(const MetaData& track, ArtistId artistId, AlbumId albumId, ArtistId albumArtistId)
{
	if (albumArtistId == -1)
	{
		albumArtistId = artistId;
	}

	const auto createdTime = !track.createdDateTime().isValid()
		                         ? Util::currentDateToInt()
		                         : track.createdDate();

	const auto modifiedTime = !track.modifiedDateTime().isValid()
		                          ? Util::currentDateToInt()
		                          : track.modifiedDate();

	auto bindings = getTrackBindings(track, artistId, albumId, albumArtistId);
	bindings[QStringLiteral("createdate")] = QVariant::fromValue(createdTime);
	bindings[QStringLiteral("modifydate")] = QVariant::fromValue(modifiedTime);

	const auto q = module()->insert(QStringLiteral("tracks"),
	                                bindings,
	                                QStringLiteral("Cannot insert track %1").arg(track.filepath()));

	return !hasError(q);
}
