/* DatabasePlaylist.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 "Database/Query.h"
#include "Database/Playlist.h"

#include "Utils/Algorithm.h"
#include "Utils/Logger/Logger.h"
#include "Utils/MetaData/MetaDataList.h"
#include "Utils/Playlist/CustomPlaylist.h"
#include "Utils/MetaData/Genre.h"
#include "Utils/Utils.h"

#include <optional>

namespace
{
	constexpr const auto PositionKey = "position";

	MetaDataList filterDisabledTracks(MetaDataList tracks)
	{
		tracks.removeTracks([](const auto& track) {
			return track.isDisabled();
		});

		return tracks;
	}

	QStringList variantToStringList(const QVariant& value, const QChar splitter)
	{
		return value.toString().split(splitter);
	}

	QString createSortorderStatement(const PlaylistSortOrder& sortorder)
	{
		switch (sortorder)
		{
			case ::Playlist::SortOrder::IDAsc:
				return QStringLiteral("playlists.playlistID ASC");
			case ::Playlist::SortOrder::IDDesc:
				return QStringLiteral("playlists.playlistID DESC");
			case ::Playlist::SortOrder::NameAsc:
				return QStringLiteral("playlists.playlist ASC");
			case ::Playlist::SortOrder::NameDesc:
				return QStringLiteral("playlists.playlist DESC");
			default:
				return {};
		}
	}

	QString createStoreTypeStatement(const ::Playlist::StoreType storeType)
	{
		switch (storeType)
		{
			case ::Playlist::StoreType::OnlyTemporary:
				return QStringLiteral("playlists.temporary = 1");
			case ::Playlist::StoreType::OnlyPermanent:
				return QStringLiteral("playlists.temporary = 0");
			default:
				return QStringLiteral("1");
		}
	}

	MetaDataList mergeTracks(MetaDataList tracks1, MetaDataList tracks2)
	{
		auto tracks = MetaDataList() << std::move(tracks1) << std::move(tracks2);

		Util::Algorithm::sort(tracks, [](const auto& track1, const auto& track2) {
			const auto pos1 = track1.customField(PositionKey).toInt();
			const auto pos2 = track2.customField(PositionKey).toInt();

			return (pos1 < pos2);
		});

		return tracks;
	}

	QString joinedPlaylistFields()
	{
		static const auto fields = QStringList
		{
			QStringLiteral("playlists.playlistID AS playlistID"),
			QStringLiteral("playlists.playlist   AS playlistName"),
			QStringLiteral("playlists.temporary  AS temporary"),
			QStringLiteral("playlists.isLocked   AS isLocked")
		};

		static const auto joinedFields = fields.join(", ");

		return joinedFields;
	}

	constexpr const auto CommaReplacement = "%2C";
	constexpr const auto ColonReplacement = "%3A";

	[[nodiscard]] std::optional<MetaData::Bookmark> stringToChapter(const QString& str)
	{
		const auto splitted = str.split(":");
		if (splitted.size() == 2)
		{
			auto title = splitted[0];
			auto hasTimestamp = false;
			const auto timestamp = splitted[1].toInt(&hasTimestamp);

			if (!title.isEmpty() && hasTimestamp && (timestamp >= 0))
			{
				title.replace(ColonReplacement, ":");
				title.replace(CommaReplacement, ",");

				return MetaData::Bookmark{title, timestamp};
			}
		}

		return std::nullopt;
	}

	void applyChapterString(MetaData& track, const QString& chapterString)
	{
		const auto splitted = chapterString.split(",");
		for (const auto& chapterStr: splitted)
		{
			if (const auto maybeChapter = stringToChapter(chapterStr); maybeChapter.has_value())
			{
				track.addBookmark(*maybeChapter);
			}
		}
	}

	[[nodiscard]] QString extractChapterString(const MetaData& track)
	{
		auto list = QStringList{};
		Util::Algorithm::transform(track.bookmarks(), list, [](auto bookmark) {
			bookmark.title.replace(":", ColonReplacement);
			bookmark.title.replace(",", CommaReplacement);

			return QStringLiteral("%1:%2")
			       .arg(bookmark.title)
			       .arg(bookmark.timestamp);
		});

		return list.join(",");
	}

	int insertOnlineTrack(DB::Module* module, const MetaData& track)
	{
		auto fieldBindings = QMap<QString, QVariant>{
			{QStringLiteral("isUpdatable"), track.isUpdatable()},
			{QStringLiteral("description"), Util::convertNotNull(track.comment())},
			{QStringLiteral("userAgent"), Util::convertNotNull(track.customField(QStringLiteral("user-agent")))},
			{QStringLiteral("radioMode"), static_cast<int>(track.radioMode())},
			{QStringLiteral("chapters"), extractChapterString(track)},
			{QStringLiteral("duration"), static_cast<int>(track.durationMs())}
		};

		if (track.radioMode() == RadioMode::Station)
		{
			fieldBindings.insert(QStringLiteral("stationName"), Util::convertNotNull(track.radioStationName()));
			fieldBindings.insert(QStringLiteral("stationUrl"), Util::convertNotNull(track.radioStation()));
		}

		if (track.radioMode() == RadioMode::Podcast)
		{
			fieldBindings.insert(QStringLiteral("podcastTitle"), Util::convertNotNull(track.title()));
			fieldBindings.insert(QStringLiteral("podcastArtist"), Util::convertNotNull(track.artist()));
			fieldBindings.insert(QStringLiteral("podcastAlbum"), Util::convertNotNull(track.album()));
			fieldBindings.insert(QStringLiteral("podcastAlbumArtist"), Util::convertNotNull(track.albumArtist()));
		}

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

		return DB::hasError(q) ? -1 : q.lastInsertId().toInt();
	}
}

DB::Playlist::Playlist(const QString& connectionName) :
	Module(connectionName)
{
	const auto query = QStringLiteral("CREATE VIEW IF NOT EXISTS playlist_view AS "
		"SELECT p.playlistID, p.playlist, p.temporary, p.isLocked, COUNT(ptt.filepath) AS count "
		"FROM playlists p LEFT JOIN playlistToTracks ptt "
		"ON p.playlistID = ptt.playlistID "
		"GROUP BY p.playlistID;");

	runQuery(query, QStringLiteral("Cannot create playlist view"));
}

DB::Playlist::~Playlist() = default;

QList<CustomPlaylist>
DB::Playlist::getAllPlaylists(::Playlist::StoreType storeType, const bool getTracks,
                              const ::Playlist::SortOrder sortOrder)
{
	QList<CustomPlaylist> result;

	const auto storeTypeStatement = createStoreTypeStatement(storeType);
	const auto sortingStatement = createSortorderStatement(sortOrder);

	const auto queryText =
		QStringLiteral("SELECT %1 "
			"FROM playlists "
			"LEFT OUTER JOIN playlistToTracks ptt ON playlists.playlistID = ptt.playlistID "
			"WHERE %2 "
			"GROUP BY playlists.playlistID "
			"ORDER BY %3;")
		.arg(joinedPlaylistFields())
		.arg(storeTypeStatement)
		.arg(sortingStatement);

	auto query = runQuery(queryText, QStringLiteral("Cannot fetch all playlists"));
	if (hasError(query))
	{
		return {};
	}

	while (query.next())
	{
		CustomPlaylist customPlaylist;
		if (query.value(0).isNull())
		{
			continue;
		}

		const auto playlistId = query.value(0).toInt();
		customPlaylist.setId(playlistId);
		customPlaylist.setName(query.value(1).toString());

		const auto isTemporary = (query.value(2) != 0);
		customPlaylist.setTemporary(isTemporary);
		customPlaylist.setLocked(query.value(3) != 0);

		if (getTracks)
		{
			auto tracks = getPlaylistWithDatabaseTracks(playlistId);
			auto nonDbTracks = getPlaylistWithNonDatabaseTracks(playlistId);
			auto mergedTracks = mergeTracks(tracks, nonDbTracks);

			customPlaylist.setTracks(std::move(mergedTracks));
		}

		result.push_back(customPlaylist);
	}

	return result;
}

CustomPlaylist DB::Playlist::getPlaylistById(const int playlistId, const bool getTracks)
{
	if (playlistId < 0)
	{
		return {};
	}

	const auto queryText = QStringLiteral("SELECT %1 "
		"FROM playlists LEFT OUTER JOIN playlistToTracks ptt "
		"ON playlists.playlistID = ptt.playlistID "
		"WHERE playlists.playlistid = :playlist_id "
		"GROUP BY playlists.playlistID;").arg(joinedPlaylistFields());

	auto query = runQuery(queryText, {{QStringLiteral(":playlist_id"), playlistId}},
	                      QStringLiteral("Cannot fetch all playlists"));
	if (hasError(query))
	{
		return {};
	}

	if (query.next())
	{
		CustomPlaylist result;

		result.setId(query.value(0).toInt());
		result.setName(query.value(1).toString());

		const auto temporary = (query.value(2) != 0);
		result.setTemporary(temporary);
		result.setLocked(query.value(3) != 0);

		if (getTracks)
		{
			auto tracks = getPlaylistWithDatabaseTracks(playlistId);
			auto nonDbTracks = getPlaylistWithNonDatabaseTracks(playlistId);

			auto mergedTracks = mergeTracks(tracks, nonDbTracks);
			result.setTracks(std::move(mergedTracks));
		}

		return result;
	}

	return {};
}

MetaDataList DB::Playlist::getPlaylistWithDatabaseTracks(const int playlistId)
{
	MetaDataList result;

	static const auto fields = QStringList
	{
		QStringLiteral("tsv.trackID          AS trackID"), // 0
		QStringLiteral("tsv.title            AS title"), // 1
		QStringLiteral("tsv.length           AS length"), // 2
		QStringLiteral("tsv.year             AS year"), // 3
		QStringLiteral("tsv.bitrate          AS bitrate"), // 4
		QStringLiteral("tsv.filename         AS filename"), // 5
		QStringLiteral("tsv.trackNum         AS trackNum"), // 6
		QStringLiteral("tsv.albumID          AS albumID"), // 7
		QStringLiteral("tsv.artistID         AS artistID"), // 8
		QStringLiteral("tsv.albumName        AS albumName"), // 9
		QStringLiteral("tsv.artistName       AS artistName"), // 10
		QStringLiteral("tsv.genre            AS genrename"), // 11
		QStringLiteral("tsv.filesize         AS filesize"), // 12
		QStringLiteral("tsv.discnumber       AS discnumber"), // 13
		QStringLiteral("tsv.rating           AS rating"), // 14
		QStringLiteral("ptt.filepath         AS filepath"), // 15
		QStringLiteral("tsv.trackLibraryId   AS libraryId"), // 16
		QStringLiteral("tsv.createdate       AS createdate"), // 17
		QStringLiteral("tsv.modifydate       AS modifydate"), // 18
		QStringLiteral("ptt.coverDownloadUrl AS coverDownloadUrl"), // 19
		QStringLiteral("ptt.position         AS position") // 20
	};

	static const auto joinedFields = fields.join(", ");

	const auto queryText = QStringLiteral("SELECT %1 "
		"FROM track_search_view tsv, playlists, playlistToTracks ptt "
		"WHERE playlists.playlistID = :playlist_id "
		"AND playlists.playlistID = ptt.playlistID "
		"AND ptt.trackID = tsv.trackID "
		"ORDER BY ptt.position ASC; ").arg(joinedFields);

	auto query = runQuery(queryText,
	                      {
		                      {QStringLiteral(":playlist_id"), playlistId}
	                      },
	                      QStringLiteral("Cannot get tracks for playlist %1").arg(playlistId)
	);

	if (!hasError(query))
	{
		while (query.next())
		{
			MetaData data;

			data.setId(query.value(0).toInt());
			data.setTitle(query.value(1).toString());
			data.setDurationMs(query.value(2).toInt());
			data.setYear(query.value(3).value<Year>());
			data.setBitrate(query.value(4).value<Bitrate>());
			data.setFilepath(query.value(5).toString());
			data.setTrackNumber(query.value(6).value<TrackNum>());
			data.setAlbumId(query.value(7).toInt());
			data.setArtistId(query.value(8).toInt());
			data.setAlbum(query.value(9).toString().trimmed());
			data.setArtist(query.value(10).toString().trimmed());
			data.setGenres(variantToStringList(query.value(11), ','));
			data.setFilesize(query.value(12).value<Filesize>());
			data.setDiscnumber(query.value(13).value<Disc>());
			data.setRating(query.value(14).value<Rating>());
			data.setLibraryid(query.value(16).value<LibraryId>());
			data.setCreatedDate(query.value(17).value<uint64_t>());
			data.setModifiedDate(query.value(18).value<uint64_t>());
			data.setCoverDownloadUrls(variantToStringList(query.value(19), ';'));
			data.addCustomField(PositionKey, QString(), QString::number(query.value(20).toInt()));

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

	return result;
}

MetaDataList DB::Playlist::getPlaylistWithNonDatabaseTracks(int playlistId)
{
	MetaDataList result;

	const auto static fields = QStringList{
		QStringLiteral("ptt.filepath"), // 0
		QStringLiteral("ptt.position"), // 1
		QStringLiteral("ptt.coverDownloadUrl"), // 2
		QStringLiteral("ot.radioMode"), // 3
		QStringLiteral("ot.stationName"), // 4
		QStringLiteral("ot.stationUrl"), // 5
		QStringLiteral("ot.isUpdatable"), // 6
		QStringLiteral("ot.userAgent"), // 7
		QStringLiteral("ot.description"), // 8
		QStringLiteral("ot.chapters"), // 9
		QStringLiteral("ot.duration"), // 10
		QStringLiteral("ot.podcastTitle"), // 11
		QStringLiteral("ot.podcastAlbum"), // 12
		QStringLiteral("ot.podcastArtist"), // 13
		QStringLiteral("ot.podcastAlbumArtist"), // 14
	};

	const auto static joinedFields = fields.join(", ");

	// ignore line if there are no tracks in ptt
	// do not ignore line if it's no track in ot
	const auto queryText =
		QStringLiteral("SELECT %1 "
			"FROM playlists pl "
			"JOIN playlistToTracks ptt ON pl.playlistId = ptt.playlistId "
			"LEFT OUTER JOIN onlineTracks ot ON ptt.onlineTrackId = ot.onlineTrackId "
			"WHERE ptt.trackID < 0 "
			"AND pl.playlistId = :playlistID "
			"ORDER BY ptt.position ASC;").arg(joinedFields);

	// non database playlists
	auto query = runQuery(
		queryText,
		{
			{QStringLiteral(":playlistID"), playlistId}
		},
		QStringLiteral("Playlist by id: Cannot fetch playlist %1").arg(playlistId));

	if (hasError(query))
	{
		return result;
	}

	while (query.next())
	{
		const auto filepath = query.value(0).toString();
		const auto position = query.value(1).toInt();
		const auto coverUrls = variantToStringList(query.value(2), ';');
		const auto radioMode = query.value(3).value<RadioMode>();
		const auto stationName = query.value(4).toString();
		const auto stationUrl = query.value(5).toString();
		const auto isUpdatable = query.value(6).toBool();
		const auto userAgent = query.value(7).toString();
		const auto description = query.value(8).toString();
		const auto chapterString = query.value(9).toString();
		const auto duration = static_cast<MilliSeconds>(query.value(10).toInt());
		const auto podcastTitle = query.value(11).toString();
		const auto podcastAlbum = query.value(12).toString();
		const auto podcastArtist = query.value(13).toString();
		const auto podcastAlbumArtist = query.value(14).toString();

		auto track = MetaData(filepath);
		track.setId(-1);
		track.setCoverDownloadUrls(coverUrls);
		track.setUpdateable(isUpdatable);
		track.addCustomField(QStringLiteral("user-agent"), "", userAgent);
		track.setComment(description);
		applyChapterString(track, chapterString);

		if (radioMode == RadioMode::Station)
		{
			track.setRadioStation(stationUrl, stationName);
		}

		else if (radioMode == RadioMode::Podcast)
		{
			const auto title = podcastTitle.isEmpty() ? stationName : podcastTitle;
			const auto artist = podcastArtist.isEmpty() ? stationUrl : podcastArtist;
			const auto album = podcastAlbum.isEmpty() ? stationName : podcastAlbum;
			const auto albumArtist = podcastAlbumArtist.isEmpty() ? artist : podcastAlbumArtist;

			track.setTitle(title);
			track.setArtist(artist);
			track.setAlbum(album);
			track.setAlbumArtist(albumArtist);
		}

		else
		{
			track.setTitle(filepath);
			track.setArtist(filepath);
		}

		track.changeRadioMode(radioMode);
		track.addCustomField(PositionKey, QString(), QString::number(position));
		track.setDurationMs(duration);

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

	return result;
}

int DB::Playlist::getPlaylistIdByName(const QString& name)
{
	const auto queryText = QStringLiteral("SELECT playlistid FROM playlists WHERE playlist = :playlistName;");
	auto query = runQuery(
		queryText,
		{
			{QStringLiteral(":playlistName"), Util::convertNotNull(name)}
		},
		QStringLiteral("Playlist by name: Cannot fetch playlist %1").arg(name)
	);

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

bool DB::Playlist::insertTrackIntoPlaylist(const MetaData& track, const int playlistId, const int pos)
{
	if (track.isDisabled())
	{
		return false;
	}

	auto fieldBindings = QMap<QString, QVariant>{
		{QStringLiteral("coverDownloadUrl"), Util::convertNotNull(track.coverDownloadUrls().join(";"))},
		{QStringLiteral("playlistid"), playlistId},
		{QStringLiteral("filepath"), Util::convertNotNull(track.filepath())},
		{QStringLiteral("position"), pos},
		{QStringLiteral("trackid"), track.id()},
	};

	if ((track.radioMode() == RadioMode::Station) ||
	    (track.radioMode() == RadioMode::Podcast))
	{
		const auto onlineTrackId = insertOnlineTrack(this, track);
		if (onlineTrackId < 0)
		{
			return false;
		}

		fieldBindings.insert(QStringLiteral("onlineTrackId"), onlineTrackId);
	}

	auto query = insert(QStringLiteral("playlistToTracks"), fieldBindings,
	                    QStringLiteral("Cannot insert track into playlist"));
	return !hasError(query);
}

int DB::Playlist::createPlaylist(const QString& playlistName, const bool temporary, const bool isLocked)
{
	const auto query = insert(QStringLiteral("playlists"),
	                          {
		                          {QStringLiteral("playlist"), Util::convertNotNull(playlistName)},
		                          {QStringLiteral("temporary"), temporary ? 1 : 0},
		                          {QStringLiteral("isLocked"), isLocked ? 1 : 0}
	                          }, QStringLiteral("Cannot create playlist"));

	return hasError(query)
		       ? -1
		       : query.lastInsertId().toInt();
}

bool DB::Playlist::updatePlaylist(const int playlistId, const QString& name, const bool temporary, const bool isLocked)
{
	const auto playlist = getPlaylistById(playlistId, false);
	const auto existingId = getPlaylistIdByName(name);
	const auto isIdValid = (playlistId >= 0);
	const auto otherPlaylistHasSameName = ((existingId >= 0) && (playlist.id() != existingId));
	if (!isIdValid || otherPlaylistHasSameName)
	{
		return false;
	}

	const auto q = update(QStringLiteral("playlists"),
	                      {
		                      {QStringLiteral("temporary"), temporary ? 1 : 0},
		                      {QStringLiteral("playlist"), Util::convertNotNull(name)},
		                      {QStringLiteral("isLocked"), isLocked ? 1 : 0}
	                      },
	                      {QStringLiteral("playlistId"), playlistId}, QStringLiteral("Cannot update playlist"));

	return wasUpdateSuccessful(q);
}

bool DB::Playlist::renamePlaylist(const int playlistId, const QString& name)
{
	const auto playlist = getPlaylistById(playlistId, false);
	const auto existingId = getPlaylistIdByName(name);
	if ((existingId != playlistId) || name.isEmpty() || (playlist.id() < 0))
	{
		return false;
	}

	const auto q = update(QStringLiteral("playlists"),
	                      {
		                      {QStringLiteral("playlist"), Util::convertNotNull(name)}
	                      },
	                      {QStringLiteral("playlistId"), playlistId}, QStringLiteral("Cannot update playlist"));

	return wasUpdateSuccessful(q);
}

bool DB::Playlist::updatePlaylistTracks(int playlistId, const MetaDataList& tracks)
{
	if (const auto playlist = getPlaylistById(playlistId, false); playlist.id() < 0)
	{
		return false;
	}

	clearPlaylist(playlistId);
	if (tracks.isEmpty())
	{
		return true;
	}

	const auto enabledTracks = filterDisabledTracks(tracks);

	db().transaction();
	auto position = 0;
	for (const auto& track: enabledTracks)
	{
		const auto success = insertTrackIntoPlaylist(track, playlistId, position);
		if (success)
		{
			position++;
		}
	}
	db().commit();

	return (enabledTracks.isEmpty() || (position > 0));
}

bool DB::Playlist::clearPlaylist(const int playlistId)
{
	const auto clearOnlineTracks = QStringLiteral(
		"DELETE FROM OnlineTracks WHERE onlineTrackId IN ("
		"SELECT onlineTrackId from playlistToTracks WHERE playlistToTracks.playlistId = :playlistId);");

	const auto clearOnlineTracksQuery =
		runQuery(clearOnlineTracks, {QStringLiteral(":playlistId"), playlistId},
		         QStringLiteral("Cannot clear online tracks"));

	if (hasError(clearOnlineTracksQuery))
	{
		return false;
	}

	const auto clearPlaylistToTracks = QStringLiteral("DELETE FROM playlistToTracks WHERE playlistID = :playlistID;");
	const auto clearPlaylistToTracksQuery =
		runQuery(clearPlaylistToTracks, {
			         QStringLiteral(":playlistID"), playlistId
		         }, QStringLiteral("Playlist cannot be cleared"));

	return !hasError(clearPlaylistToTracksQuery);
}

bool DB::Playlist::deletePlaylist(int playlistId)
{
	db().transaction();

	auto success = clearPlaylist(playlistId);

	const auto querytext = QStringLiteral("DELETE FROM playlists WHERE playlistID = :playlistID;");
	const auto query = runQuery(querytext, {
		                            QStringLiteral(":playlistID"), playlistId
	                            }, QStringLiteral("Playlist cannot be deleted"));

	success &= !hasError(query);

	if (!success)
	{
		db().rollback();
	}
	else
	{
		db().commit();
	}

	return success;
}

bool DB::Playlist::deleteEmptyTemporaryPlaylists()
{
	const auto q = runQuery(
		QStringLiteral("DELETE from playlists WHERE playlistID IN ("
			"SELECT playlistID FROM playlist_view WHERE count = 0 AND temporary = 1);"),
		QStringLiteral("Cannot delete playlist"));

	return !hasError(q);
}




