/* DatabaseAlbums.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/Albums.h"
#include "Database/Module.h"
#include "Database/Utils.h"
#include "Database/Query.h"

#include "Utils/Algorithm.h"
#include "Utils/Library/Filter.h"
#include "Utils/MetaData/Album.h"
#include "Utils/Settings/Settings.h"
#include "Utils/Utils.h"

using DB::Albums;
using Library::Filter;

namespace
{
	constexpr auto CisPlaceholder = ":cissearch";

	enum class FieldId : uint8_t
	{
		AlbumId = 0,
		AlbumName,
		AlbumRating,
		ArtistNames,
		AlbumArtistName,
		AlbumLength,
		TrackCount,
		AlbumYear,
		Discnumbers,
		Filenames,
		CreateDate,
	};

	std::underlying_type_t<FieldId> operator+(const FieldId f)
	{
		return static_cast<std::underlying_type_t<FieldId>>(f);
	}

	QStringList fieldInfos()
	{
		static const auto result = QStringList{
			QStringLiteral("albumID"),
			QStringLiteral("albumName"),
			QStringLiteral("albumRating"),
			QStringLiteral("artistNames"),
			QStringLiteral("albumArtistName"),
			QStringLiteral("albumLength"),
			QStringLiteral("trackCount"),
			QStringLiteral("albumYear"),
			QStringLiteral("discnumbers"),
			QStringLiteral("filenames"),
			QStringLiteral("createDate")
		};

		return result;
	}

	QString albumViewName(const LibraryId libraryId)
	{
		return libraryId < 0
			       ? QStringLiteral("album_view")
			       : QStringLiteral("album_view_%1").arg(QString::number(libraryId));
	}

	QList<Disc> variantToDiscnumbers(const QVariant& variant)
	{
		QList<Disc> result;
		auto discs = variant.toString().split(',');
		discs.removeDuplicates();

		for (const auto& disc: discs)
		{
			result << static_cast<Disc>(disc.toInt());
		}

		if (result.isEmpty())
		{
			result << static_cast<Disc>(1U);
		}

		return result;
	}

	QStringList appendAliases(QStringList fields, const QStringList& aliases)
	{
		for (int i = 0; i < fields.size(); i++)
		{
			fields[i] += QStringLiteral(" AS %1").arg(aliases[i]);
		}

		return fields;
	}

	void dropAlbumView(const DB::Module* module, const QString& albumView)
	{
		const auto query = QStringLiteral("DROP VIEW IF EXISTS %1;").arg(albumView);
		module->runQuery(query, QStringLiteral("Cannot drop album view"));
	}

	void createAlbumView(const DB::Module* module, const QString& trackView, const QString& albumView)
	{
		static const auto fields = appendAliases(
			{
				QStringLiteral("albums.albumID"),
				QStringLiteral("albums.name"),
				QStringLiteral("albums.rating"),
				QStringLiteral("GROUP_CONCAT(DISTINCT artists.name)"),
				QStringLiteral("GROUP_CONCAT(DISTINCT albumArtists.name)"),
				QStringLiteral("SUM(%1.length) / 1000"),
				QStringLiteral("COUNT(DISTINCT %1.trackID)"),
				QStringLiteral("MAX(%1.year)"),
				QStringLiteral("GROUP_CONCAT(DISTINCT %1.discnumber)"),
				QStringLiteral("GROUP_CONCAT(%1.filename, '#')"),
				QStringLiteral("MAX(%1.createDate)"),
			}, fieldInfos());

		const auto fieldStatement = fields.join(", ").arg(trackView);
		const auto joinStatement = QStringLiteral(
				"LEFT OUTER JOIN %1 ON %1.albumID = albums.albumID " // leave out empty albums
				"LEFT OUTER JOIN artists ON %1.artistID = artists.artistID "
				"LEFT OUTER JOIN artists albumArtists ON %1.albumArtistID = albumArtists.artistID")
			.arg(trackView);

		const auto query = QStringLiteral("CREATE VIEW %1 AS SELECT %2 FROM albums %3 GROUP BY albums.albumID;")
		                   .arg(albumView)
		                   .arg(fieldStatement)
		                   .arg(joinStatement);

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

	QString createQueryForTrackSearchView(const QString& trackSearchView, const QString& whereStatement)
	{
		static const auto trackSearchViewFields = QStringList
		{
			QStringLiteral("albumID"),
			QStringLiteral("albumName"),
			QStringLiteral("albumRating"),
			QStringLiteral("GROUP_CONCAT(DISTINCT artistName)"),
			QStringLiteral("GROUP_CONCAT(DISTINCT albumArtistName)"),
			QStringLiteral("SUM(length) / 1000 AS albumLength"),
			QStringLiteral("COUNT(DISTINCT trackID) AS trackCount"),
			QStringLiteral("MAX(year) AS albumYear"),
			QStringLiteral("GROUP_CONCAT(DISTINCT discnumber)"),
			QStringLiteral("GROUP_CONCAT(filename, '#')"),
			QStringLiteral("MAX(createDate) AS createDate"),
		};

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

		return QStringLiteral("SELECT %1 FROM %2 WHERE %3 GROUP BY albumId, albumName")
		       .arg(joinedFields)
		       .arg(trackSearchView)
		       .arg(whereStatement);
	}

	QString fetchQueryAlbums(const LibraryId libraryId, const bool alsoEmpty)
	{
		static const auto joinedFields = fieldInfos().join(QStringLiteral(", "));

		const auto whereStatement = alsoEmpty ? QStringLiteral("1") : QStringLiteral("trackCount > 0");

		return QStringLiteral("SELECT %1 FROM %2 WHERE %3 ")
		       .arg(joinedFields)
		       .arg(albumViewName(libraryId))
		       .arg(whereStatement);
	}

	[[nodiscard]] QStringList createIdPlaceholders(const int count)
	{
		auto result = QStringList();
		for (int i = 0; i < count; i++)
		{
			result << QStringLiteral(":val%1").arg(i);
		}

		return result;
	}

	void bindIds(QSqlQuery& query, const QStringList& placeholders, const QList<Id>& ids)
	{
		assert(placeholders.count() == ids.count());

		for (int i = 0; i < ids.count(); i++)
		{
			query.bindValue(placeholders[i], ids[i]);
		}
	}

	QList<Id> cleanIds(QList<Id> container)
	{
		std::sort(container.begin(), container.end());
		Util::Algorithm::remove_duplicates(container);
		return container;
	}
}

Albums::Albums() = default;
Albums::~Albums() = default;

void Albums::initViews()
{
	const auto& viewName = albumViewName(libraryId());

	module()->transaction();
	dropAlbumView(module(), viewName);
	createAlbumView(module(), trackView(), viewName);
	module()->commit();
}

AlbumList Albums::dbFetchAlbums(QSqlQuery& q) const
{
	if (!q.exec())
	{
		showError(q, QStringLiteral("Could not get all albums from database"));
		return {};
	}

	auto result = AlbumList{};

	while (q.next())
	{
		Album album;

		album.setId(q.value(+FieldId::AlbumId).value<AlbumId>());
		album.setName(q.value(+FieldId::AlbumName).toString());
		album.setRating(q.value(+FieldId::AlbumRating).value<Rating>());
		album.setArtists(q.value(+FieldId::ArtistNames).toString().split(','));
		album.setAlbumArtist(q.value(+FieldId::AlbumArtistName).toString());
		album.setDurationSec(q.value(+FieldId::AlbumLength).value<Seconds>());
		album.setSongcount(q.value(+FieldId::TrackCount).value<TrackNum>());
		album.setYear(q.value(+FieldId::AlbumYear).value<Year>());
		album.setDiscnumbers(variantToDiscnumbers(q.value(+FieldId::Discnumbers)));
		album.setPathHint(q.value(+FieldId::Filenames).toString().split("#"));
		album.setCreationDate(q.value(+FieldId::CreateDate).toULongLong());

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

	return result;
}

Album Albums::getAlbumByID(const AlbumId id) const
{
	return getAlbumByID(id, false);
}

Album Albums::getAlbumByID(const AlbumId id, const bool alsoEmpty) const
{
	if (id == -1)
	{
		return {};
	}

	const auto query = QStringLiteral("%1 AND albumID = :id GROUP BY albumID, albumName, albumRating;")
		.arg(fetchQueryAlbums(libraryId(), alsoEmpty));

	auto q = QSqlQuery(module()->db());
	q.prepare(query);
	q.bindValue(QStringLiteral(":id"), id);

	const auto albums = dbFetchAlbums(q);
	return albums.empty() ? Album{} : albums[0];
}

AlbumList Albums::getAllAlbums(const bool alsoEmpty) const
{
	const auto query = QStringLiteral("%1 GROUP BY albumID, albumName, albumRating;")
		.arg(fetchQueryAlbums(libraryId(), alsoEmpty));

	auto q = QSqlQuery(module()->db());
	q.prepare(query);

	return dbFetchAlbums(q);
}

#include "Utils/Logger/Logger.h"
AlbumList Albums::getAllAlbumsByArtist(const IdList& artistIds, const Filter& filter) const
{
	if (artistIds.isEmpty())
	{
		return {};
	}

	auto result = AlbumList{};

	const auto sortedIds = cleanIds(artistIds);

	const auto filterWhereStatement = filter.cleared()
		? QStringLiteral("1")
		: getFilterWhereStatement(filter, CisPlaceholder);

	const auto placeholders = createIdPlaceholders(sortedIds.size());
	const auto artistWhereStatement = QStringLiteral("(artistId IN (%1) OR albumArtistId IN (%1))")
		.arg(placeholders.join(", "));

	const auto whereStatement = QStringLiteral("%1 AND %2")
		.arg(filterWhereStatement)
		.arg(artistWhereStatement);

	const auto query = createQueryForTrackSearchView(trackSearchView(), whereStatement);
	spLog(Log::Info, this) << query;
	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);
		bindIds(q, placeholders, sortedIds);

		result.appendUnique(dbFetchAlbums(q));
	}

	return result;
}

AlbumList Albums::getAllAlbumsBySearchString(const Filter& filter) const
{
	const auto whereStatement = getFilterWhereStatement(filter, CisPlaceholder);
	const auto query = createQueryForTrackSearchView(trackSearchView(), whereStatement);

	const auto searchFilters = filter.searchModeFiltertext(true, GetSetting(Set::Lib_SearchMode));

	auto result = AlbumList{};
	for (const auto& searchFilter: searchFilters)
	{
		auto q = QSqlQuery(module()->db());
		q.prepare(query);
		q.bindValue(CisPlaceholder, searchFilter);

		const auto tmpList = dbFetchAlbums(q);
		result.appendUnique(tmpList);
	}

	return result;
}

AlbumId Albums::updateAlbumRating(AlbumId id, Rating rating)
{
	const auto bindings = QMap<QString, QVariant>
	{
		{QStringLiteral("rating"), QVariant::fromValue(static_cast<int>(rating))}
	};

	const auto q = module()->update(
		QStringLiteral("albums"),
		bindings,
		{QStringLiteral("albumId"), id},
		QStringLiteral("Cannot set album rating for id %1").arg(id));

	return !hasError(q) && (q.numRowsAffected() > 0)
		       ? id
		       : -1;
}

void Albums::updateAlbumCissearch()
{
	const auto albums = getAllAlbums(true);

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

	for (const auto& album: albums)
	{
		const auto cissearch = Library::convertSearchstring(album.name());

		module()->update(
			QStringLiteral("albums"),
			{
				{QStringLiteral("cissearch"), cissearch}
			},
			{QStringLiteral("albumId"), album.id()},
			QStringLiteral("Cannot update album cissearch")
		);
	}

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

AlbumId Albums::insertAlbumIntoDatabase(const QString& name)
{
	const auto cissearch = Library::convertSearchstring(name);
	const auto bindings = QMap<QString, QVariant>
	{
		{QStringLiteral("name"), Util::convertNotNull(name)},
		{QStringLiteral("cissearch"), cissearch},
		{QStringLiteral("rating"), QVariant::fromValue(static_cast<int>(Rating::Zero))}
	};

	const auto q = module()->insert(
		QStringLiteral("albums"), bindings,
		QStringLiteral("2. Cannot insert album %1").arg(name));

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

AlbumId Albums::insertAlbumIntoDatabase(const Album& album)
{
	return insertAlbumIntoDatabase(album.name());
}

void Albums::deleteAllAlbums()
{
	module()->runQuery(
		QStringLiteral("DELETE FROM albums;"),
		QStringLiteral("Could not delete all albums"));
}

void Albums::deleteOrphanedAlbums()
{
	module()->runQuery(
		QStringLiteral("DELETE FROM albums WHERE albumID in (SELECT albumId FROM album_view WHERE trackCount = 0);"),
		QStringLiteral("Cannot delete orphaned albums"));
}
