/* id3.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 "Tagging.h"
#include "Tagging/TaggingCover.h"
#include "Tagging/TaggingExtraFields.h"
#include "Tagging/TaggingUtils.h"
#include "Tagging/Models/Discnumber.h"
#include "Tagging/Models/Popularimeter.h"
#include "Tagging/FileTypeResolver.h"

#include "Utils/Algorithm.h"
#include "Utils/Utils.h"
#include "Utils/MetaData/MetaData.h"
#include "Utils/Logger/Logger.h"

#include <QFileInfo>
#include <QRegExp>
#include <QDateTime>

namespace Tagging
{
	namespace
	{
		struct ReadingProperties
		{
			TagLib::AudioProperties::ReadStyle readStyle{TagLib::AudioProperties::ReadStyle::Fast};
			bool readAudioProperties{true};
		};

		ReadingProperties getReadingProperties(Quality quality)
		{
			ReadingProperties readingProperties;

			switch (quality)
			{
				case Quality::Quality:
					readingProperties.readStyle = TagLib::AudioProperties::Accurate;
					break;
				case Quality::Standard:
					readingProperties.readStyle = TagLib::AudioProperties::Average;
					break;
				case Quality::Fast:
					readingProperties.readStyle = TagLib::AudioProperties::Fast;
					break;
				case Quality::Dirty:
					readingProperties.readStyle = TagLib::AudioProperties::Fast;
					readingProperties.readAudioProperties = false;
					break;
			};

			return readingProperties;
		}

		QDateTime determineCreatedDateUtc(const QFileInfo& fileInfo)
		{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
			const auto birthTime = fileInfo.birthTime();
			const auto createDate = birthTime.isValid()
				                        ? birthTime
				                        : fileInfo.lastModified();
#else
			const auto createDate = fileInfo.created();
#endif

			return createDate.isValid()
				       ? createDate.toUTC()
				       : fileInfo.metadataChangeTime().toUTC();
		}

		QStringList extractGenres(const QString& genreString)
		{
			auto genres = genreString.split(QRegExp(",|/|;"));
			Util::Algorithm::transform(genres, [](const auto& genre) {
				return genre.trimmed();
			});

			genres.removeDuplicates();
			genres.removeAll("");

			return genres;
		}

		template<typename T, typename Setter>
		void updateIfUnequal(const T& value1, const T& value2, const ParsedTag& tag, const Setter& setter)
		{
			if (value1 != value2)
			{
				setter(tag, value2);
			}
		}
	}

	bool Utils::getMetaDataOfFile(MetaData& track, Quality quality)
	{
		FileTypeResolver::addFileTypeResolver();

		const auto fileInfo = QFileInfo(track.filepath());
		track.setFilesize(static_cast<Filesize>(fileInfo.size()));
		track.setModifiedDate(Util::dateToInt(fileInfo.lastModified().toUTC()));
		track.setCreatedDate(Util::dateToInt(determineCreatedDateUtc(fileInfo)));

		if (fileInfo.size() <= 0)
		{
			return false;
		}

		const auto readingProperties = getReadingProperties(quality);
		auto fileRef = TagLib::FileRef(TagLib::FileName(track.filepath().toUtf8()),
		                               readingProperties.readAudioProperties,
		                               readingProperties.readStyle);

		if (!isValidFile(fileRef))
		{
			spLog(Log::Warning, "Tagging") << "Cannot open tags for " << track.filepath() << ": Err 1";
			return false;
		}

		const auto parsedTag = getParsedTagFromFileRef(fileRef);
		if (!parsedTag.tag)
		{
			return false;
		}

		const auto artist = convertString(parsedTag.tag->artist());
		const auto album = convertString(parsedTag.tag->album());
		const auto title = convertString(parsedTag.tag->title());
		const auto genre = convertString(parsedTag.tag->genre());
		const auto comment = convertString(parsedTag.tag->comment());
		const auto year = parsedTag.tag->year();
		const auto trackNumber = parsedTag.tag->track();
		const auto bitrate = (quality != Quality::Dirty)
			                     ? fileRef.audioProperties()->bitrate() * 1000
			                     : 0;

		const auto length = (quality != Quality::Dirty)
			                    ? fileRef.audioProperties()->lengthInMilliseconds()
			                    : 0;

		const auto genres = extractGenres(genre);

		track.setAlbum(album);
		track.setArtist(artist);
		track.setTitle(title.isEmpty() ? MetaData::getTitleFromFilepath(track.filepath()) : title);
		track.setDurationMs(length);
		track.setYear(Year(year));
		track.setTrackNumber(static_cast<TrackNum>(trackNumber));
		track.setBitrate(Bitrate(bitrate));
		track.setGenres(genres);
		track.setComment(comment);

		if (const auto albumArtist = readAlbumArtist(parsedTag); albumArtist.has_value())
		{
			track.setAlbumArtist(albumArtist.value());
		}

		if (const auto discnumber = readDiscnumber(parsedTag); discnumber.has_value())
		{
			track.setDiscnumber(discnumber.value().disc);
			track.setDiscCount(discnumber.value().disccount);
		}

		if (const auto popularimeter = readPopularimeter(parsedTag); popularimeter.has_value())
		{
			track.setRating(popularimeter->rating);
		}

		const auto hasCover = static_cast<int>(Tagging::hasCover(parsedTag));
		track.addCustomField("has-album-art", "", QString::number(hasCover));

		return true;
	}

	bool Utils::setMetaDataOfFile(const MetaData& track)
	{
		FileTypeResolver::addFileTypeResolver();

		const auto filepath = track.filepath();
		const auto fileInfo = QFileInfo(filepath);
		if (fileInfo.size() <= 0)
		{
			return false;
		}

		auto fileRef = TagLib::FileRef(TagLib::FileName(filepath.toUtf8()));
		if (!isValidFile(fileRef))
		{
			spLog(Log::Warning, "Tagging") << "Cannot open tags for " << track.filepath() << ": Err 2";
			return false;
		}

		const auto album = convertString(track.album());
		const auto artist = convertString(track.artist());
		const auto title = convertString(track.title());
		const auto genre = convertString(track.genresToString());
		const auto comment = convertString(track.comment());

		const auto parsedTag = getParsedTagFromFileRef(fileRef);
		if (!parsedTag.tag)
		{
			return false;
		}

		parsedTag.tag->setAlbum(album);
		parsedTag.tag->setArtist(artist);
		parsedTag.tag->setTitle(title);
		parsedTag.tag->setGenre(genre);
		parsedTag.tag->setYear(track.year());
		parsedTag.tag->setTrack(track.trackNumber());
		parsedTag.tag->setComment(comment);

		writePopularimeter(parsedTag, Models::Popularimeter("sayonara", track.rating(), 0));
		writeDiscnumber(parsedTag, Models::Discnumber(track.discnumber(), track.discCount()));
		writeAlbumArtist(parsedTag, track.albumArtist());

		const auto success = fileRef.save();
		if (!success)
		{
			spLog(Log::Warning, "Tagging") << "Could not save " << track.filepath();
		}

		return success;
	}

	bool Utils::setOnlyChangedMetaDataOfFile(const MetaData& oldTrack, const MetaData& newTrack)
	{
		FileTypeResolver::addFileTypeResolver();

		const auto filepath = newTrack.filepath();
		const auto fileInfo = QFileInfo(filepath);
		if (fileInfo.size() <= 0)
		{
			return false;
		}

		auto fileRef = TagLib::FileRef(TagLib::FileName(filepath.toUtf8()));
		if (!isValidFile(fileRef))
		{
			spLog(Log::Warning, "Tagging") << "Cannot open tags for " << newTrack.filepath() << ": Err 2";
			return false;
		}

		const auto parsedTag = getParsedTagFromFileRef(fileRef);
		if (!parsedTag.tag)
		{
			return false;
		}

		updateIfUnequal(oldTrack.album(), newTrack.album(), parsedTag, [](const auto& ptag, const auto& value) {
			ptag.tag->setAlbum(convertString(value));
		});

		updateIfUnequal(oldTrack.artist(), newTrack.artist(), parsedTag, [](const auto& ptag, const auto& value) {
			ptag.tag->setArtist(convertString(value));
		});

		updateIfUnequal(oldTrack.title(), newTrack.title(), parsedTag, [](const auto& ptag, const auto& value) {
			ptag.tag->setTitle(convertString(value));
		});

		updateIfUnequal(oldTrack.genresToString(), newTrack.genresToString(), parsedTag,
		                [](const auto& tag, const auto& value) {
			                tag.tag->setGenre(convertString(value));
		                });

		updateIfUnequal(oldTrack.year(), newTrack.year(), parsedTag, [](const auto& ptag, const auto& value) {
			ptag.tag->setYear(value);
		});

		updateIfUnequal(oldTrack.trackNumber(),
		                newTrack.trackNumber(),
		                parsedTag,
		                [](const auto& ptag, const auto& value) {
			                ptag.tag->setTrack(value);
		                });

		updateIfUnequal(oldTrack.comment(), newTrack.comment(), parsedTag, [](const auto& ptag, const auto& value) {
			ptag.tag->setComment(convertString(value));
		});

		updateIfUnequal(oldTrack.rating(), newTrack.rating(), parsedTag, [](const auto& ptag, const auto& value) {
			Tagging::writePopularimeter(ptag, Models::Popularimeter("sayonara", value, 0));
		});

		updateIfUnequal(std::pair{oldTrack.discnumber(), oldTrack.discCount()},
		                std::pair{newTrack.discnumber(), newTrack.discCount()},
		                parsedTag,
		                [](const auto& ptag, const auto& value) {
			                Tagging::writeDiscnumber(ptag, Models::Discnumber(value.first, value.second));
		                });

		updateIfUnequal(oldTrack.albumArtist(), newTrack.albumArtist(), parsedTag,
		                [](const auto& ptag, const auto& value) {
			                Tagging::writeAlbumArtist(ptag, value);
		                });

		const auto success = fileRef.save();
		if (!success)
		{
			spLog(Log::Warning, "Tagging") << "Could not save " << newTrack.filepath();
		}

		return success;
	}
}
