/*
 * 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 "smartplaylistspage.h"
#include "gui/stdactions.h"
#include "models/mpdlibrarymodel.h"
#include "mpd-interface/mpdconnection.h"
#include "playlistrulesdialog.h"
#include "smartplaylists.h"
#include "support/action.h"
#include "support/configuration.h"
#include "support/messagebox.h"
#include "support/utils.h"
#include "widgets/icons.h"
#include <QRandomGenerator>
#include <algorithm>

SmartPlaylistsPage::SmartPlaylistsPage(QWidget* p)
	: SinglePageWidget(p)
{
	addAction = new Action(Icons::self()->addNewItemIcon, tr("Add"), this);
	editAction = new Action(Icons::self()->editIcon, tr("Edit"), this);
	removeAction = new Action(Icons::self()->removeIcon, tr("Remove"), this);

	ToolButton* addBtn = new ToolButton(this);
	ToolButton* editBtn = new ToolButton(this);
	ToolButton* removeBtn = new ToolButton(this);

	addBtn->setDefaultAction(addAction);
	editBtn->setDefaultAction(editAction);
	removeBtn->setDefaultAction(removeAction);

	connect(this, SIGNAL(search(QByteArray, QString)), MPDConnection::self(), SLOT(search(QByteArray, QString)));
	connect(MPDConnection::self(), SIGNAL(searchResponse(QString, QList<Song>)), this, SLOT(searchResponse(QString, QList<Song>)));
	connect(this, SIGNAL(getRating(QString)), MPDConnection::self(), SLOT(getRating(QString)));
	connect(MPDConnection::self(), SIGNAL(rating(QString, quint8)), this, SLOT(rating(QString, quint8)));
	connect(view, SIGNAL(itemsSelected(bool)), this, SLOT(controlActions()));
	connect(view, SIGNAL(headerClicked(int)), SLOT(headerClicked(int)));
	connect(addAction, SIGNAL(triggered()), SLOT(addNew()));
	connect(editAction, SIGNAL(triggered()), SLOT(edit()));
	connect(removeAction, SIGNAL(triggered()), SLOT(remove()));

	proxy.setSourceModel(SmartPlaylists::self());
	view->setModel(&proxy);
	view->setDeleteAction(removeAction);
	view->setMode(ItemView::Mode_List);
	controlActions();
	Configuration config(metaObject()->className());
	view->load(config);
	controls = QList<QWidget*>() << addBtn << editBtn << removeBtn;
	init(ReplacePlayQueue | AppendToPlayQueue, QList<QWidget*>(), controls);

	view->addAction(editAction);
	view->addAction(removeAction);
	view->alwaysShowHeader();
	view->setInfoText(tr("A 'smart' playlist contains a set of rules to select tracks from your music library to play. "
	                     "The playlist also controls the order in which tracks are added. "
	                     "Unlike 'dynamic' playlists, the play queue is not dynamically updated.")
	                  + QLatin1String("\n\n\n") + tr("Use the + icon (below) to create a new 'smart' playlist."));
}

SmartPlaylistsPage::~SmartPlaylistsPage()
{
	Configuration config(metaObject()->className());
	view->save(config);
}

void SmartPlaylistsPage::doSearch()
{
	QString text = view->searchText().trimmed();
	proxy.update(text);
	if (proxy.enabled() && !proxy.filterText().isEmpty()) {
		view->expandAll();
	}
}

void SmartPlaylistsPage::controlActions()
{
	QModelIndexList selected = qobject_cast<RulesPlaylists*>(sender()) ? QModelIndexList() : view->selectedIndexes(false);// Dont need sorted selection here...
	StdActions::self()->enableAddToPlayQueue(1 == selected.count());
	editAction->setEnabled(1 == selected.count());
	removeAction->setEnabled(selected.count());
}

void SmartPlaylistsPage::addNew()
{
	PlaylistRulesDialog* dlg = new PlaylistRulesDialog(this, SmartPlaylists::self());
	dlg->edit(QString());
}

void SmartPlaylistsPage::edit()
{
	QModelIndexList selected = view->selectedIndexes(false);// Dont need sorted selection here...

	if (1 != selected.count()) {
		return;
	}

	PlaylistRulesDialog* dlg = new PlaylistRulesDialog(this, SmartPlaylists::self());
	dlg->edit(selected.at(0).data(Qt::DisplayRole).toString());
}

void SmartPlaylistsPage::remove()
{
	QModelIndexList selected = view->selectedIndexes();

	if (selected.isEmpty() || MessageBox::No == MessageBox::warningYesNo(this, tr("Are you sure you wish to remove the selected rules?\n\nThis cannot be undone."), tr("Remove Smart Rules"), StdGuiItem::remove(), StdGuiItem::cancel())) {
		return;
	}

	QStringList names;
	for (const QModelIndex& idx : selected) {
		names.append(idx.data(Qt::DisplayRole).toString());
	}

	for (const QString& name : names) {
		SmartPlaylists::self()->del(name);
	}
}

void SmartPlaylistsPage::headerClicked(int level)
{
	if (0 == level) {
		emit close();
	}
}

void SmartPlaylistsPage::enableWidgets(bool enable)
{
	for (QWidget* c : controls) {
		c->setEnabled(enable);
	}

	view->setEnabled(enable);
}

void SmartPlaylistsPage::searchResponse(const QString& id, const QList<Song>& songs)
{
	if (id.length() < 3 || id.mid(2).toUInt() != command.id || command.isEmpty()) {
		return;
	}

	if (id.startsWith("I:")) {
		command.songs.unite(Utils::listToSet(songs));
	}
	else if (id.startsWith("E:")) {
		command.songs.subtract(Utils::listToSet(songs));
	}

	if (command.includeRules.isEmpty()) {
		if (command.songs.isEmpty()) {
			command.clear();
			emit error(tr("Failed to locate any matching songs"));
			return;
		}
		if (command.excludeRules.isEmpty()) {
			filterCommand();
		}
		else {
			emit search(command.excludeRules.takeFirst(), "E:" + QString::number(command.id));
		}
	}
	else {
		emit search(command.includeRules.takeFirst(), "I:" + QString::number(command.id));
	}
}

void SmartPlaylistsPage::filterCommand()
{
	bool filterDuration = command.minDuration > 0 || command.maxDuration > 0;
	bool filterAge = command.maxAge > 0;

	if (filterDuration || filterAge) {
		uint maxAge = time(nullptr) - (command.maxAge * 24 * 60 * 60);
		QSet<Song> toRemove;
		for (const auto& s : command.songs) {
			if ((filterAge && s.lastModified < maxAge) || (filterDuration && ((command.minDuration > s.time || (command.maxDuration > 0 && s.time > command.maxDuration))))) {
				toRemove.insert(s);
			}
			else {
				command.toCheck.append(s.file);
			}
		}
		command.songs.subtract(toRemove);
		if (command.songs.isEmpty()) {
			command.clear();
			emit error(tr("Failed to locate any matching songs"));
			return;
		}
	}

	if (command.filterRating || command.fetchRatings || command.includeUnrated) {
		if (command.toCheck.isEmpty()) {
			for (const auto& s : command.songs) {
				command.toCheck.append(s.file);
			}
		}
		command.checking = command.toCheck.takeFirst();
		emit getRating(command.checking);
	}
	else {
		addSongsToPlayQueue();
	}
}

void SmartPlaylistsPage::rating(const QString& file, quint8 val)
{
	if (command.isEmpty() || file != command.checking) {
		return;
	}

	for (auto& s : command.songs) {
		if (s.file == file) {
			s.rating = val;
			if (command.filterRating && (val < command.ratingFrom || val > command.ratingTo) && !(command.includeUnrated && val == 0)) {
				command.songs.remove(s);
			}
			break;
		}
	}

	if (command.toCheck.isEmpty()) {
		command.checking.clear();
		addSongsToPlayQueue();
	}
	else {
		command.checking = command.toCheck.takeFirst();
		emit getRating(command.checking);
	}
}

static bool sortAscending = true;
static bool composerSort(const Song& s1, const Song& s2)
{
	const QString v1 = s1.hasComposer() ? s1.composer() : QString();
	const QString v2 = s2.hasComposer() ? s2.composer() : QString();
	int c = Utils::compare(v1, v2);
	return sortAscending ? (c < 0 || (c == 0 && s1 < s2)) : (c > 0 || (c == 0 && s1 < s2));
}

static bool artistSort(const Song& s1, const Song& s2)
{
	const QString v1 = s1.hasArtistSort() ? s1.artistSort() : s1.artist;
	const QString v2 = s2.hasArtistSort() ? s2.artistSort() : s2.artist;
	int c = Utils::compare(v1, v2);
	return sortAscending ? (c < 0 || (c == 0 && s1 < s2)) : (c > 0 || (c == 0 && s1 < s2));
}

static bool albumArtistSort(const Song& s1, const Song& s2)
{
	const QString v1 = s1.hasAlbumArtistSort() ? s1.albumArtistSort() : s1.albumArtistOrComposer();
	const QString v2 = s2.hasAlbumArtistSort() ? s2.albumArtistSort() : s2.albumArtistOrComposer();
	int c = Utils::compare(v1, v2);
	return sortAscending ? (c < 0 || (c == 0 && s1 < s2)) : (c > 0 || (c == 0 && s1 < s2));
}

static bool albumSort(const Song& s1, const Song& s2)
{
	const QString v1 = s1.hasAlbumSort() ? s1.albumSort() : s1.album;
	const QString v2 = s2.hasAlbumSort() ? s2.albumSort() : s2.album;
	int c = Utils::compare(v1, v2);
	return sortAscending ? (c < 0 || (c == 0 && s1 < s2)) : (c > 0 || (c == 0 && s1 < s2));
}

static bool titleSort(const Song& s1, const Song& s2)
{
	const QString v1 = s1.title;
	const QString v2 = s2.title;
	int c = Utils::compare(v1, v2);
	return sortAscending ? (c < 0 || (c == 0 && s1 < s2)) : (c > 0 || (c == 0 && s1 < s2));
}

static bool genreSort(const Song& s1, const Song& s2)
{
	int c = s1.compareGenres(s2);
	return sortAscending ? (c < 0 || (c == 0 && s1 < s2)) : (c > 0 || (c == 0 && s1 < s2));
}

static bool dateSort(const Song& s1, const Song& s2)
{
	return sortAscending ? (s1.year < s2.year || (s1.year == s2.year && s1 < s2)) : (s1.year > s2.year || (s1.year == s2.year && s1 < s2));
}

static bool ratingSort(const Song& s1, const Song& s2)
{
	return sortAscending ? (s1.rating < s2.rating || (s1.rating == s2.rating && s1 < s2))
						 : (s1.rating > s2.rating || (s1.rating == s2.rating && s1 < s2));
}

static bool ageSort(const Song& s1, const Song& s2)
{
	return sortAscending ? (s1.lastModified < s2.lastModified || (s1.lastModified == s2.lastModified && s1 < s2))
						 : (s1.lastModified > s2.lastModified || (s1.lastModified == s2.lastModified && s1 < s2));
}

void SmartPlaylistsPage::addSongsToPlayQueue()
{
	if (command.songs.isEmpty()) {
		command.clear();
		emit error(tr("Failed to locate any matching songs"));
		return;
	}

	QList<Song> songs = command.songs.values();
	command.songs.clear();

	sortAscending = command.orderAscending;
	switch (command.order) {
	case RulesPlaylists::Order_AlbumArtist:
		std::sort(songs.begin(), songs.end(), albumArtistSort);
		break;
	case RulesPlaylists::Order_Artist:
		std::sort(songs.begin(), songs.end(), artistSort);
		break;
	case RulesPlaylists::Order_Album:
		std::sort(songs.begin(), songs.end(), albumSort);
		break;
	case RulesPlaylists::Order_Composer:
		std::sort(songs.begin(), songs.end(), composerSort);
		break;
	case RulesPlaylists::Order_Date:
		std::sort(songs.begin(), songs.end(), dateSort);
		break;
	case RulesPlaylists::Order_Genre:
		std::sort(songs.begin(), songs.end(), genreSort);
		break;
	case RulesPlaylists::Order_Rating:
		std::sort(songs.begin(), songs.end(), ratingSort);
		break;
	case RulesPlaylists::Order_Title:
		std::sort(songs.begin(), songs.end(), titleSort);
		break;
	case RulesPlaylists::Order_Age:
		std::sort(songs.begin(), songs.end(), ageSort);
		break;
	default:
	case RulesPlaylists::Order_Random:
		std::shuffle(songs.begin(), songs.end(), *QRandomGenerator::global());
	}

	QStringList files;
	for (int i = 0; i < command.numTracks && !songs.isEmpty(); ++i) {
		files.append(songs.takeFirst().file);
	}
	if (!files.isEmpty()) {
		emit add(files, command.action, command.priority, command.decreasePriority);
		view->clearSelection();
	}
	command.clear();
}

void SmartPlaylistsPage::addSelectionToPlaylist(const QString& name, int action, quint8 priority, bool decreasePriority)
{
	if (!name.isEmpty()) {
		return;
	}

	QModelIndexList selected = view->selectedIndexes(false);
	if (1 != selected.count()) {
		return;
	}

	QModelIndex idx = proxy.mapToSource(selected.at(0));
	if (!idx.isValid()) {
		return;
	}
	RulesPlaylists::Entry pl = SmartPlaylists::self()->entry(idx.row());
	if (pl.name.isEmpty() || pl.numTracks <= 0) {
		return;
	}

	command = Command(pl, action, priority, decreasePriority, command.id + 1);

	QList<RulesPlaylists::Rule>::ConstIterator it = pl.rules.constBegin();
	QList<RulesPlaylists::Rule>::ConstIterator end = pl.rules.constEnd();
	QSet<QString> mpdGenres;

	for (; it != end; ++it) {
		QList<int> dates;
		QByteArray match = "find";
		bool isInclude = true;
		RulesPlaylists::Rule::ConstIterator rIt = (*it).constBegin();
		RulesPlaylists::Rule::ConstIterator rEnd = (*it).constEnd();
		QByteArray baseRule;
		QStringList genres;

		for (; rIt != rEnd; ++rIt) {
			if (RulesPlaylists::constDateKey == rIt.key()) {
				QStringList parts = rIt.value().trimmed().split(RulesPlaylists::constRangeSep);
				if (2 == parts.length()) {
					int from = parts.at(0).toInt();
					int to = parts.at(1).toInt();
					if (from > to) {
						for (int i = to; i <= from; ++i) {
							dates.append(i);
						}
					}
					else {
						for (int i = from; i <= to; ++i) {
							dates.append(i);
						}
					}
				}
				else if (1 == parts.length()) {
					dates.append(parts.at(0).toInt());
				}
			}
			else if (RulesPlaylists::constGenreKey == rIt.key() && rIt.value().trimmed().endsWith("*")) {
				QString find = rIt.value().left(rIt.value().length() - 1);
				if (!find.isEmpty()) {
					if (mpdGenres.isEmpty()) {
						mpdGenres = MpdLibraryModel::self()->getGenres();
					}
					for (const QString& g : mpdGenres) {
						if (g.startsWith(find, Qt::CaseInsensitive)) {
							genres.append(g);
						}
					}
				}
				if (genres.isEmpty()) {
					// No genres matching pattern - add dummy genre, so that no tracks will be found
					genres.append("XXXXXXXXX");
				}
			}
			else if (RulesPlaylists::constArtistKey == rIt.key() || RulesPlaylists::constAlbumKey == rIt.key() || RulesPlaylists::constAlbumArtistKey == rIt.key() || RulesPlaylists::constComposerKey == rIt.key() || RulesPlaylists::constCommentKey == rIt.key() || RulesPlaylists::constTitleKey == rIt.key() || RulesPlaylists::constSimilarArtistsKey == rIt.key() || RulesPlaylists::constGenreKey == rIt.key() || RulesPlaylists::constFileKey == rIt.key()) {
				baseRule += " " + rIt.key().toUtf8() + " " + MPDConnection::encodeName(rIt.value());
			}
			else if (RulesPlaylists::constExactKey == rIt.key()) {
				if ("false" == rIt.value()) {
					match = "search";
				}
			}
			else if (RulesPlaylists::constExcludeKey == rIt.key()) {
				if ("true" == rIt.value()) {
					isInclude = false;
				}
			}
		}

		if (!baseRule.isEmpty() || !genres.isEmpty() || !dates.isEmpty()) {
			QList<QByteArray> rules;
			if (genres.isEmpty()) {
				if (dates.isEmpty()) {
					rules.append(match + baseRule);
				}
				else {
					for (int d : dates) {
						rules.append(match + baseRule + " Date \"" + QByteArray::number(d) + "\"");
					}
				}
			}
			else {
				for (const QString& genre : genres) {
					QByteArray rule = match + baseRule + " Genre  " + MPDConnection::encodeName(genre);
					if (dates.isEmpty()) {
						rules.append(rule);
					}
					else {
						for (int d : dates) {
							rules.append(rule + " Date \"" + QByteArray::number(d) + "\"");
						}
					}
				}
			}
			if (!rules.isEmpty()) {
				if (isInclude) {
					command.includeRules += rules;
				}
				else {
					command.excludeRules += rules;
				}
			}
		}
	}

	command.filterRating = command.haveRating();
	command.fetchRatings = RulesPlaylists::Order_Rating == command.order;
	if (command.includeRules.isEmpty()) {
		if (command.haveRating()) {
			command.includeRules.append("RATING:" + QByteArray::number(command.ratingFrom) + ":" + QByteArray::number(command.ratingTo));
			command.filterRating = false;
			command.fetchRatings = false;
		}
		else {
			command.includeRules.append(QByteArray());
		}
	}
	emit search(command.includeRules.takeFirst(), "I:" + QString::number(command.id));
}

#include "moc_smartplaylistspage.cpp"
