/*
 * Cantata
 *
 * Copyright (c) 2011-2022 Craig Drummond <craig.p.drummond@gmail.com>
 *
 * ----
 *
 * 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 2 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "tageditor.h"
#include "mpd-interface/cuefile.h"
#include "mpd-interface/mpdconnection.h"
#include "support/icon.h"
#include "support/inputdialog.h"
#include "support/messagebox.h"
#include "support/utils.h"
#include "tags.h"
#include "trackorganiser.h"
#include "widgets/tagspinbox.h"
#ifdef ENABLE_DEVICES_SUPPORT
#include "devices/device.h"
#include "models/devicesmodel.h"
#endif
#include <QCloseEvent>
#include <QCoreApplication>
#include <QDir>
#include <QEventLoop>
#include <QMenu>
#include <QTimer>
#include <algorithm>

#define REMOVE(w)         \
	w->setVisible(false); \
	w->deleteLater();     \
	w = 0;

//
// NOTE: Cantata does NOT read, and store, comments from MPD. For comment support, Cantata will read these from the
// files when the tag editor is opened.
//
static bool equalTags(const Song& a, const Song& b, bool compareCommon, bool composerSupport, bool commentSupport)
{
	return (compareCommon || a.track == b.track) && a.year == b.year && a.disc == b.disc && a.artist == b.artist && a.genres[0] == b.genres[0] && a.album == b.album && a.albumartist == b.albumartist && (!composerSupport || a.composer() == b.composer()) && (!commentSupport || a.comment() == b.comment()) && (compareCommon || a.title == b.title);
}

static bool setString(QString& str, const QString& v, bool skipEmpty)
{
	if (!skipEmpty || !v.isEmpty()) {
		str = v;
		return true;
	}
	return false;
}

static QString trim(QString str)
{
	str = str.trimmed();
	str = str.simplified();
	str = str.replace(QLatin1String(" ;"), QLatin1String(";"));
	str = str.replace(QLatin1String("; "), QLatin1String(";"));
	return str;
}

// Split genres back out
static void splitGenres(Song& song)
{
	QStringList genres = song.genres[0].split(",", CANTATA_SKIP_EMPTY);
	for (int i = 0; i < Song::constNumGenres; ++i) {
		song.genres[i] = i < genres.count() ? genres.at(i).trimmed() : QString();
	}
}

static int iCount = 0;

int TagEditor::instanceCount()
{
	return iCount;
}

static inline bool nullRating(const quint8& rating)
{
	return rating > Song::Rating_Max;
}

static inline bool nullRating(const Song& s)
{
	return nullRating(s.rating);
}

TagEditor::TagEditor(QWidget* parent, const QList<Song>& songs,
                     const QSet<QString>& existingArtists, const QSet<QString>& existingAlbumArtists, const QSet<QString>& existingComposers,
                     const QSet<QString>& existingAlbums, const QSet<QString>& existingGenres, const QString& udi)
	: SongDialog(parent)
#ifdef ENABLE_DEVICES_SUPPORT
	  ,
	  deviceUdi(udi)
#endif
	  ,
	  currentSongIndex(-1), updating(false), haveArtists(false), haveAlbumArtists(false), haveComposers(false), haveComments(false), haveAlbums(false), haveGenres(false), haveDiscs(false), haveYears(false), haveRatings(false), saving(false), composerSupport(false), commentSupport(false), readRatingsAct(nullptr), writeRatingsAct(nullptr)
{
	iCount++;
	bool ratingsSupport = false;
#ifdef ENABLE_DEVICES_SUPPORT
	if (deviceUdi.isEmpty()) {
		baseDir = MPDConnection::self()->getDetails().dir;
		composerSupport = MPDConnection::self()->composerTagSupported();
		commentSupport = MPDConnection::self()->commentTagSupported();
		ratingsSupport = MPDConnection::self()->stickersSupported();
	}
	else {
		Device* dev = getDevice(udi, parentWidget());

		if (!dev) {
			deleteLater();
			return;
		}

		baseDir = dev->path();
	}
#else
	baseDir = MPDConnection::self()->getDetails().dir;
	composerSupport = MPDConnection::self()->composerTagSupported();
	commentSupport = MPDConnection::self()->commentTagSupported();
	ratingsSupport = MPDConnection::self()->stickersSupported();
#endif

	for (const Song& s : songs) {
		if (CueFile::isCue(s.file)) {
			continue;
		}
		Song song(s);
		song.rating = Song::Rating_Null;
		if (s.guessed) {
			song.revertGuessedTags();
		}
		// Store all Genres's in 1st genre
		song.genres[0] = song.displayGenre();
		song.genres[1] = QString();
		original.append(song);
		if (s.isLocalFile()) {
			ratingsSupport = false;
			composerSupport = true;
			commentSupport = false;
			baseDir = QString();
		}
	}

	if (original.isEmpty()) {
		deleteLater();
		return;
	}
	std::sort(original.begin(), original.end());

	if (!songsOk(original, baseDir, udi.isEmpty())) {
		return;
	}

	QWidget* mainWidet = new QWidget(this);
	setupUi(mainWidet);
	if (!ratingsSupport) {
		REMOVE(ratingWidget);
		REMOVE(ratingLabel);
		REMOVE(ratingVarious);
		REMOVE(ratingNoteLabel);
	}
	else {
		connect(this, SIGNAL(getRating(QString)), MPDConnection::self(), SLOT(getRating(QString)));
		connect(this, SIGNAL(setRating(QString, quint8)), MPDConnection::self(), SLOT(setRating(QString, quint8)));
		connect(MPDConnection::self(), SIGNAL(rating(QString, quint8)), this, SLOT(rating(QString, quint8)));
		ratingWidget->setShowZeroForNull(true);
		QColor col = palette().color(QPalette::WindowText);
		ratingVarious->setStyleSheet(QString("QLabel{color:rgba(%1,%2,%3,128);}").arg(col.red()).arg(col.green()).arg(col.blue()));
	}
	setMainWidget(mainWidet);
	ButtonCodes buttons = Ok | Cancel | Reset | User3;
	if (songs.count() > 1) {
		buttons |= User2 | User1;
	}
	setButtons(buttons);
	setCaption(tr("Tags"));
	if (songs.count() > 1) {
		setButtonGuiItem(User2, StdGuiItem::back(true));
		setButtonGuiItem(User1, StdGuiItem::forward(true));
		enableButton(User1, false);
		enableButton(User2, false);
	}
	setButtonGuiItem(Ok, StdGuiItem::save());
	setButtonGuiItem(User3, GuiItem(tr("Tools"), fa::fa_solid, fa::fa_magic));
	QMenu* toolsMenu = new QMenu(this);
	toolsMenu->addAction(tr("Apply \"Various Artists\" Workaround"), this, SLOT(applyVa()));
	toolsMenu->addAction(tr("Revert \"Various Artists\" Workaround"), this, SLOT(revertVa()));
	toolsMenu->addAction(tr("Set 'Album Artist' from 'Artist'"), this, SLOT(setAlbumArtistFromArtist()));
	toolsMenu->addAction(tr("Capitalize"), this, SLOT(capitalise()));
	toolsMenu->addAction(tr("Adjust Track Numbers"), this, SLOT(adjustTrackNumbers()));
	if (ratingsSupport) {
		readRatingsAct = toolsMenu->addAction(tr("Read Ratings from File"), this, SLOT(readRatings()));
		writeRatingsAct = toolsMenu->addAction(tr("Write Ratings to File"), this, SLOT(writeRatings()));
		readRatingsAct->setEnabled(false);
		writeRatingsAct->setEnabled(false);
	}
	setButtonMenu(User3, toolsMenu, InstantPopup);
	enableButton(Ok, false);
	enableButton(Reset, false);

	setAttribute(Qt::WA_DeleteOnClose);

	QStringList strings = existingArtists.values();
	strings.sort();
	artist->clear();
	artist->insertItems(0, strings);

	strings = existingAlbumArtists.values();
	strings.sort();
	albumArtist->clear();
	albumArtist->insertItems(0, strings);

	if (composerSupport) {
		strings = existingComposers.values();
		strings.sort();
		composer->clear();
		composer->insertItems(0, strings);
	}
	else {
		REMOVE(composer)
		REMOVE(composerLabel)
	}

	if (commentSupport) {
		comment->clear();
		composer->insertItems(0, strings);
	}
	else {
		REMOVE(comment)
		REMOVE(commentLabel)
	}

	strings = existingAlbums.values();
	strings.sort();
	album->clear();
	album->insertItems(0, strings);

	strings = existingGenres.values();
	strings.sort();
	genre->clear();
	genre->insertItems(0, strings);

	trackName->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
	trackName->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow);
	trackName->view()->setTextElideMode(Qt::ElideLeft);

	if (original.count() > 1) {
		QSet<QString> songArtists;
		QSet<QString> songAlbumArtists;
		QSet<QString> songAlbums;
		QSet<QString> songGenres;
		QSet<QString> songComposers;
		QSet<QString> songComments;
		QSet<int> songYears;
		QSet<int> songDiscs;

		for (const Song& s : original) {
			songArtists.insert(s.artist);
			songAlbumArtists.insert(s.albumartist);
			songAlbums.insert(s.album);
			for (int i = 0; i < Song::constNumGenres && !s.genres[i].isEmpty(); ++i) {
				songGenres.insert(s.genres[i]);
			}
			if (composerSupport) {
				songComposers.insert(s.composer());
			}
			if (commentSupport) {
				songComments.insert(s.comment());
			}
			songYears.insert(s.year);
			songDiscs.insert(s.disc);
			if (songArtists.count() > 1 && songAlbumArtists.count() > 1 && songAlbums.count() > 1 && songGenres.count() > 1 && songYears.count() > 1 && songDiscs.count() > 1 && (!composerSupport || songComposers.count() > 1) && (!commentSupport || songComments.count() > 1)) {
				break;
			}
		}
		Song all;
		all.file.clear();
		all.title.clear();
		all.track = 0;
		all.artist = 1 == songArtists.count() ? *(songArtists.begin()) : QString();
		if (1 == songComposers.count()) {
			all.setComposer(*(songComposers.begin()));
		}
		all.setComment(1 == songComments.count() ? *(songComments.begin()) : QString());
		all.albumartist = 1 == songAlbumArtists.count() ? *(songAlbumArtists.begin()) : QString();
		all.album = 1 == songAlbums.count() ? *(songAlbums.begin()) : QString();
		all.genres[0] = 1 == songGenres.count() ? *(songGenres.begin()) : QString();
		all.year = 1 == songYears.count() ? *(songYears.begin()) : 0;
		all.disc = 1 == songDiscs.count() ? *(songDiscs.begin()) : 0;
		all.rating = Song::Rating_Null;
		original.prepend(all);
		artist->setFocus();
		haveArtists = songArtists.count() > 1;
		haveAlbumArtists = songAlbumArtists.count() > 1;
		haveAlbums = songAlbums.count() > 1;
		haveGenres = songGenres.count() > 1;
		haveComposers = songComposers.count() > 1;
		haveDiscs = songDiscs.count() > 1;
		haveYears = songYears.count() > 1;
	}
	else {
		title->setFocus();
	}
	edited = original;
	setIndex(0);

	if (ratingsSupport || commentSupport) {
		progress->setVisible(true);
		progress->setRange(0, (original.count() > 1 ? (original.count() - 1) : 1) * (commentSupport && ratingsSupport ? 2 : 1));
		progress->setValue(0);
	}
	else {
		progress->setVisible(false);
	}

	bool first = original.count() > 1;
	for (const Song& s : original) {
		if (first) {
			trackName->insertItem(trackName->count(), tr("All tracks"));
			first = false;
		}
		else {
			trackName->insertItem(trackName->count(), s.filePath());
			if (ratingsSupport) {
				emit getRating(s.file);
			}
		}
	}
	connect(title, SIGNAL(textChanged(const QString&)), SLOT(checkChanged()));
	connect(artist, SIGNAL(activated(int)), SLOT(checkChanged()));
	connect(artist, SIGNAL(editTextChanged(const QString&)), SLOT(checkChanged()));
	connect(albumArtist, SIGNAL(activated(int)), SLOT(checkChanged()));
	connect(albumArtist, SIGNAL(editTextChanged(const QString&)), SLOT(checkChanged()));
	if (composerSupport) {
		connect(composer, SIGNAL(activated(int)), SLOT(checkChanged()));
		connect(composer, SIGNAL(editTextChanged(const QString&)), SLOT(checkChanged()));
	}
	if (commentSupport) {
		connect(comment, SIGNAL(textChanged(const QString&)), SLOT(checkChanged()));
	}
	connect(album, SIGNAL(activated(int)), SLOT(checkChanged()));
	connect(album, SIGNAL(editTextChanged(const QString&)), SLOT(checkChanged()));
	connect(genre, SIGNAL(activated(int)), SLOT(checkChanged()));
	connect(track, SIGNAL(valueChanged(int)), SLOT(checkChanged()));
	connect(disc, SIGNAL(valueChanged(int)), SLOT(checkChanged()));
	connect(genre, SIGNAL(editTextChanged(const QString&)), SLOT(checkChanged()));
	connect(year, SIGNAL(valueChanged(int)), SLOT(checkChanged()));
	connect(trackName, SIGNAL(activated(int)), SLOT(setIndex(int)));
	connect(this, SIGNAL(update()), MPDConnection::self(), SLOT(updateMaybe()));
	if (ratingWidget) {
		connect(ratingWidget, SIGNAL(valueChanged(int)), SLOT(checkRating()));
	}
	adjustSize();
	int w = Utils::scaleForDpi(600);
	if (width() < w) {
		resize(w, height());
	}
	if (commentSupport) {
		QTimer::singleShot(0, this, SLOT(readComments()));
	}
	trackName->lineEdit()->setReadOnly(true);
}

TagEditor::~TagEditor()
{
	iCount--;
}

void TagEditor::fillSong(Song& s, bool isAll, bool skipEmpty) const
{
	Song all = original.at(0);
	bool haveAll = original.count() > 1;

	if (!isAll) {
		setString(s.title, title->text().trimmed(), skipEmpty);
	}
	setString(s.artist, artist->text().trimmed(), skipEmpty && (!haveAll || all.artist.isEmpty()));
	setString(s.album, album->text().trimmed(), skipEmpty && (!haveAll || all.album.isEmpty()));
	setString(s.albumartist, albumArtist->text().trimmed(), skipEmpty && (!haveAll || all.albumartist.isEmpty()));
	if (composerSupport) {
		QString str;
		if (setString(str, composer->text().trimmed(), skipEmpty && (!haveAll || all.composer().isEmpty()))) {
			s.setComposer(str);
		}
	}
	if (commentSupport) {
		QString str;
		if (setString(str, comment->text().trimmed(), skipEmpty && (!haveAll || all.comment().isEmpty()))) {
			s.setComment(str);
		}
	}
	if (!isAll) {
		s.track = track->value();
	}
	if (!isAll || 0 != disc->value()) {
		s.disc = disc->value();
	}
	setString(s.genres[0], trim(genre->text()), skipEmpty && (!haveAll || all.genres[0].isEmpty()));
	if (!isAll || 0 != year->value()) {
		s.year = year->value();
	}
	if (ratingWidget) {
		s.rating = ratingWidget->value();
	}
}

void TagEditor::setVariousHint()
{
	if (0 == currentSongIndex && original.count() > 1) {
		Song all = original.at(0);
		artist->setPlaceholderText(all.artist.isEmpty() && haveArtists ? TagSpinBox::variousStr() : QString());
		album->setPlaceholderText(all.album.isEmpty() && haveAlbums ? TagSpinBox::variousStr() : QString());
		albumArtist->setPlaceholderText(all.albumartist.isEmpty() && haveAlbumArtists ? TagSpinBox::variousStr() : QString());
		if (composerSupport) {
			composer->setPlaceholderText(all.composer().isEmpty() && haveComposers ? TagSpinBox::variousStr() : QString());
		}
		if (commentSupport) {
			comment->setPlaceholderText(all.comment().isEmpty() && haveComments ? TagSpinBox::variousStr() : QString());
		}
		genre->setPlaceholderText(all.genres[0].isEmpty() && haveGenres ? TagSpinBox::variousStr() : QString());
		disc->setVarious(0 == all.disc && haveDiscs);
		year->setVarious(0 == all.year && haveYears);
		if (ratingVarious) {
			ratingVarious->setVisible(nullRating(all) && haveRatings);
		}
	}
	else {
		artist->setPlaceholderText(QString());
		album->setPlaceholderText(QString());
		albumArtist->setPlaceholderText(QString());
		if (composerSupport) {
			composer->setPlaceholderText(QString());
		}
		if (commentSupport) {
			comment->setPlaceholderText(QString());
		}
		genre->setPlaceholderText(QString());
		disc->setVarious(false);
		year->setVarious(false);
		if (ratingVarious) {
			ratingVarious->setVisible(false);
		}
	}
}

void TagEditor::enableOkButton()
{
	enableButton(Ok, (editedIndexes.count() > 1) || (1 == original.count() && 1 == editedIndexes.count()) || (1 == editedIndexes.count() && !editedIndexes.contains(0)));
	enableButton(Reset, isButtonEnabled(Ok));
}

void TagEditor::setLabelStates()
{
	Song o = original.at(currentSongIndex);
	Song e = edited.at(currentSongIndex);
	bool isAll = 0 == currentSongIndex && original.count() > 1;

	titleLabel->setOn(!isAll && o.title != e.title);
	artistLabel->setOn(o.artist != e.artist);
	if (composerSupport) {
		composerLabel->setOn(o.composer() != e.composer());
	}
	if (commentSupport) {
		commentLabel->setOn(o.comment() != e.comment());
	}
	albumArtistLabel->setOn(o.albumartist != e.albumartist);
	albumLabel->setOn(o.album != e.album);
	trackLabel->setOn(!isAll && o.track != e.track);
	discLabel->setOn(o.disc != e.disc);
	genreLabel->setOn(o.genres[0] != e.genres[0]);
	yearLabel->setOn(o.year != e.year);
	if (ratingLabel) {
		ratingLabel->setOn(o.rating <= Song::Rating_Max && e.rating <= Song::Rating_Max && o.rating != e.rating);
	}
}

void TagEditor::readComments()
{
	bool haveMultiple = original.count() > 1;
	bool updated = false;
	bool multipleComments = false;
	QString allComment;

	for (int i = 0; i < original.count(); ++i) {
		if (0 != i || !haveMultiple) {
			progress->setValue(progress->value() + 1);
		}
		if (i && 0 == i % 10) {
			QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
		}

		if (0 == i && haveMultiple) {
			continue;
		}
		Song song = original.at(i);
		QString comment = Tags::readComment(song.filePath(baseDir));
		if (!comment.isEmpty()) {
			song.setComment(comment);
			original.replace(i, song);
			haveComments = true;
			updated = true;
			if (haveMultiple) {
				if (allComment.isEmpty()) {
					allComment = comment;
				}
				else if (comment != allComment) {
					multipleComments = true;
				}
			}
		}
	}
	if (updated) {
		edited = original;
		if (haveMultiple && !allComment.isEmpty() && !multipleComments) {
			edited[0].setComment(allComment);
			original[0].setComment(allComment);
			if (0 == currentSongIndex) {
				comment->setText(allComment);
			}
		}
	}
	controlInitialActionsState();
	if (haveMultiple) {
		setVariousHint();
	}
	else {
		setSong(original.at(0));
	}
}

void TagEditor::applyVa()
{
	bool isAll = 0 == currentSongIndex && original.count() > 1;

	if (MessageBox::No == MessageBox::questionYesNo(this, (isAll ? tr("Apply \"Various Artists\" workaround to <b>all</b> tracks?") : tr("Apply \"Various Artists\" workaround?")) + QLatin1String("<br/><br/>") + tr("<i>This will set 'Album artist' and 'Artist' to "
	                                                                                                                                                                                                                  "\"Various Artists\", and set 'Title' to "
	                                                                                                                                                                                                                  "\"TrackArtist - TrackTitle\"</i>"),
	                                                tr("Apply \"Various Artists\" Workaround"), StdGuiItem::apply(), StdGuiItem::cancel())) {
		return;
	}

	if (isAll) {
		updating = true;
		for (int i = 0; i < edited.count(); ++i) {
			Song s = edited.at(i);
			if (s.fixVariousArtists()) {
				if (0 == i && QLatin1String(" - ") == s.title) {
					s.title = QString();
				}
				edited.replace(i, s);
				updateEditedStatus(i);
				if (i == currentSongIndex) {
					setSong(s);
				}
			}
		}
		updating = false;
		setLabelStates();
		enableOkButton();
	}
	else {
		Song s = edited.at(currentSongIndex);
		if (s.fixVariousArtists()) {
			edited.replace(currentSongIndex, s);
			updateEditedStatus(currentSongIndex);
			setSong(s);
		}
	}
}

void TagEditor::revertVa()
{
	bool isAll = 0 == currentSongIndex && original.count() > 1;

	if (MessageBox::No == MessageBox::questionYesNo(this, (isAll ? tr("Revert \"Various Artists\" workaround on <b>all</b> tracks?") : tr("Revert \"Various Artists\" workaround")) + QLatin1String("<br/><br/>") + tr("<i>Where the 'Album artist' is the same as 'Artist' "
	                                                                                                                                                                                                                   "and the 'Title' is of the format \"TrackArtist - TrackTitle\", "
	                                                                                                                                                                                                                   "'Artist' will be taken from 'Title' and 'Title' itself will be "
	                                                                                                                                                                                                                   "set to just the title. e.g. <br/><br/>"
	                                                                                                                                                                                                                   "If 'Title' is \"Wibble - Wobble\", then 'Artist' will be set to "
	                                                                                                                                                                                                                   "\"Wibble\" and 'Title' will be set to \"Wobble\"</i>"),
	                                                tr("Revert \"Various Artists\" Workaround"), GuiItem(tr("Revert")), StdGuiItem::cancel())) {
		return;
	}

	if (isAll) {
		updating = true;
		QSet<QString> artists;
		for (int i = 1; i < edited.count(); ++i) {
			Song s = edited.at(i);
			if (s.revertVariousArtists()) {
				artists.insert(s.artist);
				edited.replace(i, s);
				updateEditedStatus(i);
				if (i == currentSongIndex) {
					setSong(s);
				}
			}
		}
		Song s = edited.at(0);
		s.artist = artists.count() != 1 ? QString() : *artists.constBegin();
		edited.replace(0, s);
		updateEditedStatus(0);
		setSong(s);
		updating = false;
		setLabelStates();
		enableOkButton();
	}
	else {
		Song s = edited.at(currentSongIndex);
		if (s.revertVariousArtists()) {
			edited.replace(currentSongIndex, s);
			updateEditedStatus(currentSongIndex);
			setSong(s);
			enableOkButton();
		}
	}
}

void TagEditor::setAlbumArtistFromArtist()
{
	bool isAll = 0 == currentSongIndex && original.count() > 1;

	if (MessageBox::No == MessageBox::questionYesNo(this, isAll ? tr("Set 'Album Artist' from 'Artist' (if 'Album Artist' is empty) for <b>all</b> tracks?") : tr("Set 'Album Artist' from 'Artist' (if 'Album Artist' is empty)?"), tr("Album Artist from Artist"), StdGuiItem::apply(), StdGuiItem::cancel())) {
		return;
	}

	if (isAll) {
		updating = true;
		for (int i = 0; i < edited.count(); ++i) {
			Song s = edited.at(i);
			if (s.setAlbumArtist()) {
				edited.replace(i, s);
				updateEditedStatus(i);
				if (i == currentSongIndex) {
					setSong(s);
				}
			}
		}
		updating = false;
		setLabelStates();
		enableOkButton();
	}
	else {
		Song s = edited.at(currentSongIndex);
		if (s.setAlbumArtist()) {
			edited.replace(currentSongIndex, s);
			updateEditedStatus(currentSongIndex);
			setSong(s);
		}
	}
}

void TagEditor::capitalise()
{
	bool isAll = 0 == currentSongIndex && original.count() > 1;

	if (MessageBox::No == MessageBox::questionYesNo(this, isAll ? tr("Capitalize the first letter of text fields (e.g. 'Title', 'Artist', etc) "
	                                                                 "of <b>all</b> tracks?")
	                                                            : tr("Capitalize the first letter of text fields (e.g. 'Title', 'Artist', etc)?"),
	                                                tr("Capitalize"), GuiItem(tr("Capitalize")), StdGuiItem::cancel())) {
		return;
	}

	if (isAll) {
		for (int i = 0; i < edited.count(); ++i) {
			Song s = edited.at(i);
			if (s.capitalise()) {
				edited.replace(i, s);
				updateEditedStatus(i);
				if (i == currentSongIndex) {
					setSong(s);
				}
			}
		}
	}
	else {
		Song s = edited.at(currentSongIndex);
		if (s.capitalise()) {
			edited.replace(currentSongIndex, s);
			updateEditedStatus(currentSongIndex);
			setSong(s);
		}
	}
}

void TagEditor::adjustTrackNumbers()
{
	bool isAll = 0 == currentSongIndex && original.count() > 1;
	bool ok = false;
	int adj = InputDialog::getInteger(tr("Adjust Track Numbers"), isAll ? tr("Adjust the value of each track number by:") : tr("Adjust track number by:"),
	                                  0, -500, 500, 1, 10, &ok, this);

	if (!ok || 0 == adj) {
		return;
	}

	if (isAll) {
		for (int i = 1; i < edited.count(); ++i) {
			Song s = edited.at(i);
			s.track += adj;
			edited.replace(i, s);
			updateEditedStatus(i);
			if (i == currentSongIndex) {
				setSong(s);
			}
		}
	}
	else {
		Song s = edited.at(currentSongIndex);
		s.track += adj;
		edited.replace(currentSongIndex, s);
		updateEditedStatus(currentSongIndex);
		setSong(s);
	}
}

void TagEditor::readRatings()
{
	bool isAll = 0 == currentSongIndex && original.count() > 1;

	if (MessageBox::No == MessageBox::questionYesNo(this, isAll ? tr("Read ratings for all tracks from the music files?") : tr("Read rating from music file?"), tr("Ratings"), isAll ? GuiItem(tr("Read Ratings")) : GuiItem(tr("Read Rating")), StdGuiItem::cancel())) {
		return;
	}

	if (isAll) {
		progress->setVisible(true);
		progress->setRange(0, original.count());
		QStringList updated;
		for (int i = 1; i < original.count(); ++i) {
			progress->setValue(i + 1);
			if (i && 0 == i % 10) {
				QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
			}
			Song s = edited.at(i);
			int r = Tags::readRating(s.filePath(baseDir));
			if (r >= 0 && r <= Song::Rating_Max && s.rating != r) {
				s.rating = r;
				edited.replace(i, s);
				updated.append(s.file);
				editedIndexes.insert(i);
				updateTrackName(i, true);
				if (i == currentSongIndex) {
					setSong(s);
				}
			}
		}
		progress->setVisible(false);
		if (!updated.isEmpty()) {
			MessageBox::informationList(this, tr("Read, and updated, ratings from the following tracks:"), updated);
		}
	}
	else {
		Song s = edited.at(currentSongIndex);
		int r = Tags::readRating(s.filePath(baseDir));
		if (r >= 0 && r <= Song::Rating_Max && s.rating != r) {
			s.rating = r;
			edited.replace(currentSongIndex, s);
			setSong(s);
		}
	}
	enableOkButton();
}

void TagEditor::writeRatings()
{
	bool isAll = 0 == currentSongIndex && original.count() > 1;

	if (isAll) {
		for (int i = 1; i < original.count(); ++i) {
			if (nullRating(original.at(i))) {
				MessageBox::error(this, tr("Not all Song ratings have been read from MPD!") + QLatin1String("\n\n") + tr("Song ratings are not stored in the song files, but within MPD's 'sticker' database. "
				                                                                                                         "In order to save these into the actual file, Cantata must first read them from MPD."));
				return;
			}
		}
	}
	else {
		if (nullRating(original.at(currentSongIndex))) {
			MessageBox::error(this, tr("Song rating has not been read from MPD!") + QLatin1String("\n\n") + tr("Song ratings are not stored in the song files, but within MPD's 'sticker' database. "
			                                                                                                   "In order to save these into the actual file, Cantata must first read them from MPD."));
			return;
		}
	}

	if (MessageBox::No == MessageBox::questionYesNo(this, isAll ? tr("Write ratings for all tracks to the music files?") : tr("Write rating to music file?"), tr("Ratings"), isAll ? GuiItem(tr("Write Ratings")) : GuiItem(tr("Write Rating")), StdGuiItem::cancel())) {
		return;
	}

	if (isAll) {
		progress->setVisible(true);
		progress->setRange(0, edited.count());
		QStringList failed;
		for (int i = 1; i < edited.count(); ++i) {
			progress->setValue(i + 1);
			if (i && 0 == i % 10) {
				QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
			}
			Song s = edited.at(i);
			if (s.rating <= Song::Rating_Max) {
				Tags::Update status = Tags::updateRating(s.filePath(baseDir), s.rating);
				if (Tags::Update_Failed == status || Tags::Update_BadFile == status) {
					failed.append(s.file);
				}
			}
		}
		progress->setVisible(false);
		if (!failed.isEmpty()) {
			MessageBox::errorList(this, tr("Failed to write ratings of the following tracks:"), failed);
		}
	}
	else {
		Song s = edited.at(currentSongIndex);
		if (s.rating <= Song::Rating_Max) {
			Tags::Update status = Tags::updateRating(s.filePath(baseDir), s.rating);
			if (Tags::Update_Failed == status || Tags::Update_BadFile == status) {
				MessageBox::error(this, tr("Failed to write rating to music file!"));
			}
		}
	}
}

void TagEditor::checkChanged()
{
	if (updating) {
		return;
	}

	bool allWasEdited = editedIndexes.contains(0);

	updateEdited();

	bool allEdited = editedIndexes.contains(0);
	bool isAll = 0 == currentSongIndex && original.count() > 1;

	if (isAll && (allEdited || allWasEdited)) {
		int save = currentSongIndex;
		for (int i = 1; i < edited.count(); ++i) {
			currentSongIndex = i;
			updateEdited(true);
		}
		currentSongIndex = save;
	}
	enableOkButton();
	setLabelStates();
}

void TagEditor::updateTrackName(int index, bool edited)
{
	bool isAll = 0 == index && original.count() > 1;

	if (edited) {
		if (isAll) {
			trackName->setItemText(index, tr("All tracks [modified]"));
		}
		else {
			trackName->setItemText(index, tr("%1 [modified]").arg(original.at(index).filePath()));
		}
	}
	else {
		if (isAll) {
			trackName->setItemText(index, tr("All tracks"));
		}
		else {
			trackName->setItemText(index, original.at(index).filePath());
		}
	}
}

void TagEditor::updateEditedStatus(int index)
{
	bool isAll = 0 == index && original.count() > 1;
	Song s = edited.at(index);
	if (equalTags(s, original.at(index), isAll, composerSupport, commentSupport) && s.rating == original.at(index).rating) {
		if (editedIndexes.contains(index)) {
			editedIndexes.remove(index);
			updateTrackName(index, false);
			edited.replace(index, s);
		}
	}
	else {
		if (!editedIndexes.contains(index)) {
			editedIndexes.insert(index);
			updateTrackName(index, true);
		}
		edited.replace(index, s);
	}
}

void TagEditor::updateEdited(bool isFromAll)
{
	Song s = edited.at(currentSongIndex);
	bool isAll = 0 == currentSongIndex && original.count() > 1;
	fillSong(s, isFromAll || isAll, /*isFromAll*/ false);

	if (!isAll && isFromAll && original.count() > 1) {
		Song all = original.at(0);
		Song o = original.at(currentSongIndex);
		if (all.artist.isEmpty() && s.artist.isEmpty() && !o.artist.isEmpty()) {
			s.artist = o.artist;
		}
		if (all.albumartist.isEmpty() && s.albumartist.isEmpty() && !o.albumartist.isEmpty()) {
			s.albumartist = o.albumartist;
		}
		if (composerSupport && all.composer().isEmpty() && s.composer().isEmpty() && !o.composer().isEmpty()) {
			s.setComposer(o.composer());
		}
		if (commentSupport && all.comment().isEmpty() && s.comment().isEmpty() && !o.comment().isEmpty()) {
			s.setComment(o.comment());
		}
		if (all.album.isEmpty() && s.album.isEmpty() && !o.album.isEmpty()) {
			s.album = o.album;
		}
		if (all.genres[0].isEmpty() && s.genres[0].isEmpty() && !o.genres[0].isEmpty()) {
			s.genres[0] = o.genres[0];
		}
	}

	if (equalTags(s, original.at(currentSongIndex), isFromAll || isAll, composerSupport, commentSupport) && s.rating == original.at(currentSongIndex).rating) {
		if (editedIndexes.contains(currentSongIndex)) {
			editedIndexes.remove(currentSongIndex);
			updateTrackName(currentSongIndex, false);
			edited.replace(currentSongIndex, s);
		}
	}
	else {
		if (!editedIndexes.contains(currentSongIndex)) {
			editedIndexes.insert(currentSongIndex);
			updateTrackName(currentSongIndex, true);
		}
		edited.replace(currentSongIndex, s);
	}
}

void TagEditor::setSong(const Song& s)
{
	blockSignals(true);
	title->setText(s.title);
	artist->setText(s.artist);
	albumArtist->setText(s.albumartist);
	if (composerSupport) {
		composer->setText(s.composer());
	}
	if (commentSupport) {
		comment->setText(s.comment());
	}
	album->setText(s.album);
	track->setValue(s.track);
	disc->setValue(s.disc);
	genre->setText(s.genres[0]);
	year->setValue(s.year);
	if (ratingWidget) {
		ratingWidget->setValue(s.rating);
	}
	blockSignals(false);
	checkChanged();
}

void TagEditor::setIndex(int idx)
{
	if (currentSongIndex == idx || idx > (original.count() - 1)) {
		return;
	}
	updating = true;

	bool haveMultiple = original.count() > 1;

	if (haveMultiple && currentSongIndex >= 0) {
		updateEdited();
	}
	Song s = edited.at(!haveMultiple || idx == 0 ? 0 : idx);
	setSong(s);
	currentSongIndex = idx;

	bool isMultiple = haveMultiple && 0 == idx;
	title->setEnabled(!isMultiple);
	titleLabel->setEnabled(!isMultiple);
	track->setEnabled(!isMultiple);
	trackLabel->setEnabled(!isMultiple);

	if (isMultiple) {
		title->setText(QString());
		track->setValue(0);
	}

	if (original.count() > 1) {
		enableButton(User1, !isMultiple && idx < (original.count() - 1));// Next
		enableButton(User2, !isMultiple && idx > 1);                     // Prev
	}
	setVariousHint();
	enableOkButton();
	trackName->setCurrentIndex(idx);
	setLabelStates();
	updating = false;
}

void TagEditor::rating(const QString& f, quint8 r)
{
	if (!ratingWidget) {
		return;
	}
	for (int i = original.count() > 1 ? 1 : 0; i < original.count(); ++i) {
		Song s = original.at(i);
		if (nullRating(s) && s.rating != r && s.file == f) {
			progress->setValue(progress->value() + 1);
			controlInitialActionsState();
			s.rating = r;
			original.replace(i, s);
			s = edited.at(i);
			if (nullRating(s)) {
				s.rating = r;
				edited.replace(i, s);
			}
			if (i == currentSongIndex && ratingWidget->value() > Song::Rating_Max) {
				ratingWidget->setValue(r);
			}
		}
	}

	if (original.count() > 1 && !haveRatings) {
		quint8 rating = Song::Rating_Null;
		bool first = true;
		for (int i = 1; i < original.count() && !haveRatings; ++i) {
			quint8 r = original.at(i).rating;
			if (nullRating(r)) {
				continue;
			}
			if (first) {
				rating = r;
				first = false;
			}
			else if (r != rating) {
				haveRatings = true;
				rating = Song::Rating_Null;
			}
		}
		Song s = original.at(0);
		if (s.rating != rating) {
			s.rating = rating;
			original.replace(0, s);
		}
		s = edited.at(0);
		if (s.rating != rating) {
			s.rating = rating;
			edited.replace(0, s);
		}
		if (0 == currentSongIndex) {
			ratingWidget->setValue(rating);
		}
		setVariousHint();
	}
}

void TagEditor::checkRating()
{
	if (!ratingWidget) {
		return;
	}
	checkChanged();
	if (original.count() > 1 && 0 == currentSongIndex) {
		quint8 rating = Song::Rating_Null;
		bool first = true;
		haveRatings = false;
		for (int i = 1; i < edited.count() && !haveRatings; ++i) {
			quint8 r = edited.at(i).rating;
			if (nullRating(r)) {
				continue;
			}
			if (first) {
				rating = r;
				first = false;
			}
			else if (r != rating) {
				haveRatings = true;
				rating = Song::Rating_Null;
			}
		}
		Song s = edited.at(0);
		if (s.rating != rating) {
			s.rating = rating;
			edited.replace(0, s);
			if (0 == currentSongIndex) {
				ratingWidget->setValue(rating);
			}
		}
		setVariousHint();
	}
}

bool TagEditor::applyUpdates()
{
	bool skipFirst = original.count() > 1;
	QStringList failed;
	DeviceOptions opts;
	QString udi;
#ifdef ENABLE_DEVICES_SUPPORT
	Device* dev = nullptr;
	if (!deviceUdi.isEmpty()) {
		dev = getDevice(deviceUdi, this);
		if (!dev) {
			return true;
		}
		opts = dev->options();
		udi = dev->id();
	}
	else
#endif
		opts.load(MPDConnectionDetails::configGroupName(MPDConnection::self()->getDetails().name), true);

	int toSave = editedIndexes.count();
	bool renameFiles = false;
	QList<Song> updatedSongs;

	saving = true;
	enableButton(Ok, false);
	enableButton(Cancel, false);
	enableButton(Reset, false);
	enableButton(User1, false);
	enableButton(User2, false);
	enableButton(User3, false);
	progress->setVisible(true);
	progress->setRange(1, toSave);

	int count = 0;
	bool someTimedout = false;
	bool isLocal = false;
	for (int idx : editedIndexes) {
		progress->setValue(progress->value() + 1);

		if (0 == count++ % 10) {
			QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
		}

		if (skipFirst && 0 == idx) {
			continue;
		}
		Song orig = original.at(idx);
		Song edit = edited.at(idx);

		if (orig.isLocalFile()) {
			isLocal = true;
		}
		if (ratingWidget && orig.rating != edit.rating && edit.rating <= Song::Rating_Max) {
			emit setRating(orig.file, edit.rating);
		}

		if (equalTags(orig, edit, false, composerSupport, commentSupport)) {
			continue;
		}

		QString file = orig.filePath();
		QString afile = orig.filePath(baseDir);
		splitGenres(orig);
		splitGenres(edit);
		switch (Tags::update(afile, orig, edit, -1, commentSupport)) {
		case Tags::Update_Modified:
			edit.setComment(QString());
#ifdef ENABLE_DEVICES_SUPPORT
			if (!deviceUdi.isEmpty()) {
				if (!dev->updateSong(orig, edit)) {
					dev->removeSongFromList(orig);
					dev->addSongToList(edit);
				}
			}
			else
#endif
			{
				//                if (!MusicLibraryModel::self()->updateSong(orig, edit)) {
				//                    MusicLibraryModel::self()->removeSongFromList(orig);
				//                    MusicLibraryModel::self()->addSongToList(edit);
				//                }
			}
			updatedSongs.append(edit);
			if (!renameFiles && !isLocal && file != opts.createFilename(edit)) {
				renameFiles = true;
			}
			break;
		case Tags::Update_Failed:
			failed.append(file);
			break;
		case Tags::Update_BadFile:
			failed.append(tr("%1 (Corrupt tags?)", "filename (Corrupt tags?)").arg(file));
			break;
		default:
			break;
		}
	}
	saving = false;

	if (failed.count()) {
		MessageBox::errorListEx(this, tr("Failed to update the tags of the following tracks:"), failed);
	}

	if (updatedSongs.count()) {
		// If we call tag-editor, no need to do MPD update - as this will be done from that dialog...
		if (renameFiles && MessageBox::Yes == MessageBox::questionYesNo(this, tr("Would you also like to rename your song files, so as to match your tags?"), tr("Rename Files"), GuiItem(tr("Rename")), StdGuiItem::cancel())) {
			TrackOrganiser* dlg = new TrackOrganiser(parentWidget());
			dlg->show(updatedSongs, udi, true);
		}
		else {
#ifdef ENABLE_DEVICES_SUPPORT
			if (!deviceUdi.isEmpty()) {
				dev->saveCache();
			}
			else
#endif
					if (!isLocal) {
				//             MusicLibraryModel::self()->removeCache();
				emit update();
			}
		}
	}

	return !someTimedout;
}

void TagEditor::slotButtonClicked(int button)
{
	switch (button) {
	case Ok: {
		if (applyUpdates()) {
			accept();
		}
		break;
	}
	case Reset:// Reset
		if (0 == currentSongIndex && original.count() > 1) {
			for (int i = 0; i < original.count(); ++i) {
				edited.replace(i, original.at(i));
				updateTrackName(i, false);
			}
			editedIndexes.clear();
			setSong(original.at(currentSongIndex));
			checkRating();
		}
		else {
			setSong(original.at(currentSongIndex));
		}
		setLabelStates();
		enableOkButton();
		break;
	case User1:// Next
		setIndex(currentSongIndex + 1);
		break;
	case User2:// Prev
		setIndex(currentSongIndex - 1);
		break;
	case Cancel:
		reject();
		// Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
		Dialog::slotButtonClicked(button);
		break;
	default:
		break;
	}
}

#ifdef ENABLE_DEVICES_SUPPORT
Device* TagEditor::getDevice(const QString& udi, QWidget* p)
{
	Device* dev = DevicesModel::self()->device(udi);
	if (!dev) {
		MessageBox::error(p ? p : this, tr("Device has been removed!"));
		reject();
		return nullptr;
	}
	if (!dev->isConnected()) {
		MessageBox::error(p ? p : this, tr("Device is not connected."));
		reject();
		return nullptr;
	}
	if (!dev->isIdle()) {
		MessageBox::error(p ? p : this, tr("Device is busy?"));
		reject();
		return nullptr;
	}
	return dev;
}
#endif

void TagEditor::closeEvent(QCloseEvent* event)
{
	if (saving) {
		event->ignore();
	}
	else {
		Dialog::closeEvent(event);
	}
}

void TagEditor::controlInitialActionsState()
{
	if (progress->value() >= progress->maximum()) {
		progress->setVisible(false);
		if (readRatingsAct) {
			readRatingsAct->setEnabled(true);
		}
		if (writeRatingsAct) {
			writeRatingsAct->setEnabled(true);
		}
	}
}

#include "moc_tageditor.cpp"
