/*
 * 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 "songview.h"
#include "contextengine.h"
#include "gui/covers.h"
#include "gui/settings.h"
#include "lyricsdialog.h"
#include "support/messagebox.h"
#include "support/squeezedtextlabel.h"
#include "support/utils.h"
#include "ultimatelyrics.h"
#include "ultimatelyricsprovider.h"
#ifdef TagLib_FOUND
#include "tags/tags.h"
#endif
#include "gui/stdactions.h"
#include "mpd-interface/mpdstatus.h"
#include "network/networkaccessmanager.h"
#include "qtiocompressor/qtiocompressor.h"
#include "support/action.h"
#include "support/actioncollection.h"
#include "support/utils.h"
#include "widgets/icons.h"
#include "widgets/textbrowser.h"
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMenu>
#include <QScrollBar>
#include <QTextStream>
#include <QTimer>
#include <QUrl>
#include <QUrlQuery>

const QLatin1String SongView::constLyricsDir("lyrics/");
const QLatin1String SongView::constExtension(".lyrics");
const QLatin1String SongView::constCacheDir("tracks/");
const QLatin1String SongView::constInfoExt(".html.gz");

static QString infoCacheFileName(const Song& song, const QString& lang, bool createDir)
{
	QString dir = Utils::cacheDir(SongView::constCacheDir + Covers::encodeName(song.basicArtist()) + Utils::constDirSep, createDir);
	if (dir.isEmpty()) {
		return QString();
	}
	return dir + Covers::encodeName(song.basicTitle()) + "." + lang + SongView::constInfoExt;
}

static QString lyricsCacheFileName(const Song& song, bool createDir = false)
{
	QString dir = Utils::cacheDir(SongView::constLyricsDir + Covers::encodeName(song.basicArtist()) + Utils::constDirSep, createDir);
	if (dir.isEmpty()) {
		return QString();
	}
	return dir + Covers::encodeName(song.basicTitle()) + SongView::constExtension;
}

#if !defined Q_OS_WIN && !defined Q_OS_MAC
static QString lyricsOtherFileName(const Song& song)
{
	return QDir::homePath() + "/.lyrics/" + Covers::encodeName(song.basicArtist()) + " - " + Covers::encodeName(song.basicTitle()) + ".txt";
}
#endif

static inline QString mpdLyricsFilePath(const Song& song)
{
	return Utils::changeExtension(song.filePath(MPDConnection::self()->getDetails().dir), SongView::constExtension);
}

static inline QString fixNewLines(const QString& o)
{
	return QString(o).replace("\\n", "\n").replace("\\t", " ").replace("\t", " ").replace(QLatin1String("\n\n\n"), QLatin1String("\n\n")).replace("\n", "<br/>");
}

SongView::SongView(QWidget* p)
	: View(p, QStringList() << tr("Lyrics") << tr("Information") << tr("Metadata")), scrollTimer(nullptr), songPos(0), currentProvider(-1), currentRequest(0), mode(Mode_Display), job(nullptr), currentProv(nullptr), lyricsNeedsUpdating(true), infoNeedsUpdating(true), metadataNeedsUpdating(true)
{
	scrollAction = ActionCollection::get()->createAction("scrolllyrics", tr("Scroll Lyrics"), Icons::self()->downIcon);
	refreshAction = ActionCollection::get()->createAction("refreshlyrics", tr("Refresh Lyrics"), Icons::self()->refreshIcon);
	editAction = ActionCollection::get()->createAction("editlyrics", tr("Edit Lyrics"), Icons::self()->editIcon);
	delAction = ActionCollection::get()->createAction("dellyrics", tr("Delete Lyrics File"), Icons::self()->removeIcon);

	scrollAction->setCheckable(true);
	scrollAction->setChecked(Settings::self()->contextAutoScroll());
	connect(scrollAction, SIGNAL(toggled(bool)), SLOT(toggleScroll()));
	connect(refreshAction, SIGNAL(triggered()), SLOT(update()));
	connect(editAction, SIGNAL(triggered()), SLOT(edit()));
	connect(delAction, SIGNAL(triggered()), SLOT(del()));
	connect(UltimateLyrics::self(), SIGNAL(lyricsReady(int, QString)), SLOT(lyricsReady(int, QString)));

	engine = ContextEngine::create(this);
	refreshInfoAction = ActionCollection::get()->createAction("refreshtrack", tr("Refresh Track Information"), Icons::self()->refreshIcon);
	cancelInfoJobAction = new Action(Icons::self()->cancelIcon, tr("Cancel"), this);
	cancelInfoJobAction->setEnabled(false);
	connect(refreshInfoAction, SIGNAL(triggered()), SLOT(refreshInfo()));
	connect(cancelInfoJobAction, SIGNAL(triggered()), SLOT(abortInfoSearch()));
	connect(engine, SIGNAL(searchResult(QString, QString)), this, SLOT(infoSearchResponse(QString, QString)));
	for (TextBrowser* t : texts) {
		connect(t, SIGNAL(anchorClicked(QUrl)), SLOT(showMoreInfo(QUrl)));
	}

	text->setContextMenuPolicy(Qt::CustomContextMenu);
	connect(text, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showContextMenu(QPoint)));
	texts.at(Page_Information)->setContextMenuPolicy(Qt::CustomContextMenu);
	connect(texts.at(Page_Information), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showInfoContextMenu(QPoint)));
	connect(this, SIGNAL(viewChanged()), this, SLOT(curentViewChanged()));
	setMode(Mode_Blank);
	setStandardHeader(tr("Track"));
	clear();
	toggleScroll();
	setCurrentView(Settings::self()->contextTrackView());
}

SongView::~SongView()
{
	UltimateLyrics::self()->release();
}

void SongView::update()
{
	QString mpdName = mpdFileName();
	QString cacheName = cacheFileName();
	bool mpdExists = !mpdName.isEmpty() && QFile::exists(mpdName);
	bool cacheExists = !cacheName.isEmpty() && QFile::exists(cacheName);

	if (mpdExists || cacheExists) {
		switch (MessageBox::warningYesNoCancel(this, tr("Reload lyrics?\n\nReload from disk, or delete disk copy and download?"), tr("Reload"),
		                                       GuiItem(tr("Reload From Disk")), GuiItem(tr("Download")))) {
		case MessageBox::Yes:
			break;
		case MessageBox::No:
			if (mpdExists) {
				QFile::remove(mpdName);
			}
			if (cacheExists) {
				QFile::remove(cacheName);
			}
			break;
		default:
			return;
		}
	}

	update(currentSong, true);
}

void SongView::saveConfig()
{
	Settings::self()->saveContextAutoScroll(scrollAction->isChecked());
	Settings::self()->saveContextTrackView(currentView());
}

void SongView::search()
{
	setMode(Mode_Display);
	Song song = currentSong;
	LyricsDialog dlg(currentSong, this);
	if (QDialog::Accepted == dlg.exec()) {
		if ((song.artist != currentSong.artist || song.title != currentSong.title) && MessageBox::No == MessageBox::warningYesNo(this, tr("Current playing song has changed, still perform search?"), tr("Song Changed"), GuiItem(tr("Perform Search")), StdGuiItem::cancel())) {
			return;
		}
		QString mpdName = mpdFileName();
		if (!mpdName.isEmpty() && QFile::exists(mpdName)) {
			QFile::remove(mpdName);
		}
		QString cacheName = cacheFileName();
		if (!cacheName.isEmpty() && QFile::exists(cacheName)) {
			QFile::remove(cacheName);
		}
		update(dlg.song(), true);
	}
}

void SongView::edit()
{
	if (lyricsFile.isEmpty()) {
		lyricsFile = lyricsCacheFileName(currentSong, true);
	}
	if (!QFile::exists(lyricsFile)) {
		QFile file(lyricsFile);
		if (file.open(QIODevice::ReadWrite | QIODevice::Text)) {
			QTextStream stream(&file);
			stream << tr("Failed to locate lyrics") << CANTATA_ENDL;
		}
	}
	QDesktopServices::openUrl(QUrl::fromLocalFile(lyricsFile));
}

void SongView::del()
{
	if (MessageBox::No == MessageBox::warningYesNo(this, tr("Delete lyrics file?"), tr("Delete File"), StdGuiItem::del(), StdGuiItem::cancel())) {
		return;
	}

	QString mpdName = mpdFileName();
	QString cacheName = cacheFileName();
	if (!cacheName.isEmpty() && QFile::exists(cacheName)) {
		QFile::remove(cacheName);
	}
	if (!mpdName.isEmpty() && QFile::exists(mpdName)) {
		QFile::remove(mpdName);
	}
}

void SongView::showContextMenu(const QPoint& pos)
{
	QMenu* menu = text->createStandardContextMenu();
	switch (mode) {
	case Mode_Blank:
		break;
	case Mode_Display:
		menu->addSeparator();
		menu->addAction(scrollAction);
		menu->addAction(refreshAction);
		menu->addAction(StdActions::self()->searchAction);
		menu->addSeparator();
		menu->addAction(editAction);
		menu->addAction(delAction);
		break;
	}

	if (cancelJobAction->isEnabled()) {
		menu->addSeparator();
		menu->addAction(cancelJobAction);
	}

	menu->exec(text->mapToGlobal(pos));
	delete menu;
}

void SongView::showInfoContextMenu(const QPoint& pos)
{
	QMenu* menu = texts.at(Page_Information)->createStandardContextMenu();
	menu->addSeparator();
	if (cancelInfoJobAction->isEnabled()) {
		menu->addAction(cancelInfoJobAction);
	}
	else {
		menu->addAction(refreshInfoAction);
	}
	menu->exec(texts.at(Page_Information)->mapToGlobal(pos));
	delete menu;
}

void SongView::toggleScroll()
{
	if (scrollAction->isChecked()) {
		scrollTimer = new QTimer(this);
		scrollTimer->setSingleShot(false);
		scrollTimer->setInterval(1000);
		connect(MPDStatus::self(), SIGNAL(updated()), this, SLOT(songPosition()));
		connect(scrollTimer, SIGNAL(timeout()), this, SLOT(scroll()));
		scroll();
	}
	else {
		disconnect(MPDStatus::self(), SIGNAL(updated()), this, SLOT(songPosition()));
		if (scrollTimer) {
			connect(scrollTimer, SIGNAL(timeout()), this, SLOT(scroll()));
			scrollTimer->stop();
		}
	}
}

void SongView::songPosition()
{
	if (scrollTimer) {
		songPos = MPDStatus::self()->timeElapsed();
		scroll();
	}
}

void SongView::scroll()
{
	if (Mode_Display == mode && scrollAction->isChecked() && scrollTimer) {
		QScrollBar* bar = text->verticalScrollBar();

		if (bar && bar->isSliderDown()) {
			scrollTimer->stop();
			return;
		}
		if (MPDStatus::self()->timeTotal() <= 0) {
			scrollTimer->stop();
			return;
		}
		if (MPDState_Playing == MPDStatus::self()->state()) {
			if (!scrollTimer->isActive()) {
				scrollTimer->start();
			}
		}
		else {
			scrollTimer->stop();
		}

		if (MPDStatus::self()->guessedElapsed() >= MPDStatus::self()->timeTotal()) {
			if (scrollTimer) {
				scrollTimer->stop();
			}
			return;
		}

		if (bar->isVisible()) {
			int newSliderPosition =
					MPDStatus::self()->guessedElapsed() * (bar->maximum() + bar->pageStep()) / MPDStatus::self()->timeTotal() - bar->pageStep() / 2;
			bar->setSliderPosition(newSliderPosition);
		}
	}
}

void SongView::curentViewChanged()
{
	if (!isVisible()) {
		return;
	}
	switch (currentView()) {
	case Page_Lyrics: loadLyrics(); break;
	case Page_Information: loadInfo(); break;
	case Page_Metadata: loadMetadata(); break;
	default: break;
	}
}

void SongView::loadLyrics()
{
	if (!lyricsNeedsUpdating) {
		return;
	}
	lyricsNeedsUpdating = false;

	if (currentSong.isCantataStream() || (!MPDConnection::self()->getDetails().dir.isEmpty() && !currentSong.file.isEmpty() && !currentSong.isNonMPD())) {
		QString songFile = currentSong.filePath(MPDConnection::self()->getDetails().dir);
		QString mpdLyrics = mpdLyricsFilePath(currentSong);

		if (!currentSong.isCantataStream() && MPDConnection::self()->getDetails().dir.startsWith(QLatin1String("http:/"))) {
			QUrl url(mpdLyrics);
			job = NetworkAccessManager::self()->get(url);
			job->setProperty("file", currentSong.file);
			connect(job, SIGNAL(finished()), this, SLOT(downloadFinished()));
			return;
		}
	}

	loadLyricsFromFile();
}

void SongView::loadLyricsFromFile()
{
	if (currentSong.isCantataStream() || (!MPDConnection::self()->getDetails().dir.isEmpty() && !currentSong.file.isEmpty() && !currentSong.isNonMPD())) {
		QString songFile = currentSong.filePath(MPDConnection::self()->getDetails().dir);
		QString mpdLyrics = mpdLyricsFilePath(currentSong);

#ifdef TagLib_FOUND
		QString tagLyrics = Tags::readLyrics(songFile);

		if (!tagLyrics.isEmpty()) {
			text->setText(fixNewLines(tagLyrics));
			setMode(Mode_Display);
			//                    controls->setVisible(false);
			return;
		}
#endif
		// Stop here if we found lyrics in the cache dir.
		if (setLyricsFromFile(mpdLyrics)) {
			lyricsFile = mpdLyrics;
			setMode(Mode_Display);
			return;
		}
		// Try .txt extension
		mpdLyrics = Utils::changeExtension(mpdLyrics, ".txt");
		if (setLyricsFromFile(mpdLyrics)) {
			lyricsFile = mpdLyrics;
			setMode(Mode_Display);
			return;
		}
	}

	// Check for cached file...
	QString file = lyricsCacheFileName(currentSong);

	if (setLyricsFromFile(file)) {
		// We just wanted a normal update without explicit re-fetching. We can return
		// here because we got cached lyrics and we don't want an explicit re-fetch.
		lyricsFile = file;
		setMode(Mode_Display);
		return;
	}

	file = Utils::changeExtension(file, ".txt");
	if (setLyricsFromFile(file)) {
		// We just wanted a normal update without explicit re-fetching. We can return
		// here because we got cached lyrics and we don't want an explicit re-fetch.
		lyricsFile = file;
		setMode(Mode_Display);
		return;
	}

#if !defined Q_OS_WIN && !defined Q_OS_MAC
	file = lyricsOtherFileName(currentSong);
	if (setLyricsFromFile(file)) {
		// We just wanted a normal update without explicit re-fetching. We can return
		// here because we got cached lyrics and we don't want an explicit re-fetch.
		lyricsFile = file;
		setMode(Mode_Display);
		return;
	}
#endif

	getLyrics();
}

void SongView::loadInfo()
{
	if (!infoNeedsUpdating) {
		return;
	}
	infoNeedsUpdating = false;
	for (const QString& lang : engine->getLangs()) {
		QString prefix = engine->getPrefix(lang);
		QString cachedFile = infoCacheFileName(currentSong, prefix, false);
		if (QFile::exists(cachedFile)) {
			QFile f(cachedFile);
			QtIOCompressor compressor(&f);
			compressor.setStreamFormat(QtIOCompressor::GzipFormat);
			if (compressor.open(QIODevice::ReadOnly)) {
				QByteArray data = compressor.readAll();

				if (!data.isEmpty()) {
					infoSearchResponse(QString::fromUtf8(data), QString());
					Utils::touchFile(cachedFile);
					return;
				}
			}
		}
	}
	searchForInfo();
}

static inline QString fixNewLine(QString s)
{
	return s.replace(QLatin1String("\n"), QLatin1String("<br/>"));
}

static QString createRow(const QString& key, const QString& value, bool htmlEscape = true)
{
	return value.isEmpty()
			? QString()
			: QString("<tr><td>%1:&nbsp;</td><td>%2</td></tr>").arg(key).arg(fixNewLine(htmlEscape ? value.toHtmlEscaped() : value));
}

struct MapEntry {
	MapEntry(int v = 0, const QString& s = QString()) : val(v), str(s) {}
	int val;
	QString str;
};

#ifdef TagLib_FOUND
static QString clean(QString v)
{
	if (v.length() > 1) {
		v.replace("_", " ");
		v = v[0] + v.toLower().mid(1);
	}
	return v;
}
#endif

static void addIfNotEmpty(QMultiMap<int, QString>& tags, int pos, const QString& key, const QString& val)
{
	if (!val.isEmpty()) {
		tags.insert(pos, createRow(key, val));
	}
}

void SongView::loadMetadata()
{
	if (!metadataNeedsUpdating) {
		return;
	}
	metadataNeedsUpdating = false;

	QMultiMap<int, QString> tags;
	QMultiMap<int, QString> audioProperties;
#ifdef TagLib_FOUND
	if (currentSong.isCantataStream() || (!currentSong.isStandardStream() && !MPDConnection::self()->getDetails().dir.startsWith(QLatin1String("http:/")))) {
		QString songFile = currentSong.filePath(MPDConnection::self()->getDetails().dir);
		if (!songFile.isEmpty()) {
			static QMap<QString, MapEntry> tagMap;
			static QMap<QString, MapEntry> tagTimeMap;
			static const QString constTitle = QLatin1String("TITLE");
			static const QString constPerformer = QLatin1String("PERFORMER:");
			static const QString constAudio = QLatin1String("X-AUDIO:");

			if (tagMap.isEmpty()) {
				int pos = 0;
				tagMap.insert(QLatin1String("ARTIST"), MapEntry(pos++, tr("Artist")));
				tagMap.insert(QLatin1String("ALBUMARTIST"), MapEntry(pos++, tr("Album artist")));
				tagMap.insert(QLatin1String("COMPOSER"), MapEntry(pos++, tr("Composer")));
				pos++;// For performer...
				tagMap.insert(QLatin1String("LYRICIST"), MapEntry(pos++, tr("Lyricist")));
				tagMap.insert(QLatin1String("CONDUCTOR"), MapEntry(pos++, tr("Conductor")));
				tagMap.insert(QLatin1String("REMIXER"), MapEntry(pos++, tr("Remixer")));
				tagMap.insert(QLatin1String("ALBUM"), MapEntry(pos++, tr("Album")));
				tagMap.insert(QLatin1String("SUBTITLE"), MapEntry(pos++, tr("Subtitle")));
				tagMap.insert(QLatin1String("TRACKNUMBER"), MapEntry(pos++, tr("Track number")));
				tagMap.insert(QLatin1String("DISCNUMBER"), MapEntry(pos++, tr("Disc number")));
				tagMap.insert(QLatin1String("GENRE"), MapEntry(pos++, tr("Genre")));
				tagMap.insert(QLatin1String("DATE"), MapEntry(pos++, tr("Date")));
				tagMap.insert(QLatin1String("ORIGINALDATE"), MapEntry(pos++, tr("Original date")));
				tagMap.insert(QLatin1String("COMMENT"), MapEntry(pos++, tr("Comment")));
				tagMap.insert(QLatin1String("COPYRIGHT"), MapEntry(pos++, tr("Copyright")));
				tagMap.insert(QLatin1String("LABEL"), MapEntry(pos++, tr("Label")));
				tagMap.insert(QLatin1String("CATALOGNUMBER"), MapEntry(pos++, tr("Catalogue number")));
				tagMap.insert(QLatin1String("TITLESORT"), MapEntry(pos++, tr("Title sort")));
				tagMap.insert(QLatin1String("ARTISTSORT"), MapEntry(pos++, tr("Artist sort")));
				tagMap.insert(QLatin1String("ALBUMARTISTSORT"), MapEntry(pos++, tr("Album artist sort")));
				tagMap.insert(QLatin1String("ALBUMSORT"), MapEntry(pos++, tr("Album sort")));
				tagMap.insert(QLatin1String("ENCODEDBY"), MapEntry(pos++, tr("Encoded by")));
				tagMap.insert(QLatin1String("ENCODING"), MapEntry(pos++, tr("Encoder")));
				tagMap.insert(QLatin1String("MOOD"), MapEntry(pos++, tr("Mood")));
				tagMap.insert(QLatin1String("MEDIA"), MapEntry(pos++, tr("Media")));
				tagMap.insert(constAudio + QLatin1String("BITRATE"), MapEntry(pos++, tr("Bitrate")));
				tagMap.insert(constAudio + QLatin1String("SAMPLERATE"), MapEntry(pos++, tr("Sample rate")));
				tagMap.insert(constAudio + QLatin1String("CHANNELS"), MapEntry(pos++, tr("Channels")));

				tagTimeMap.insert(QLatin1String("TAGGING TIME"), MapEntry(pos++, tr("Tagging time")));
			}

			QMap<QString, QString> allTags = Tags::readAll(songFile);

			if (!allTags.isEmpty()) {
				QMap<QString, QString>::ConstIterator it = allTags.constBegin();
				QMap<QString, QString>::ConstIterator end = allTags.constEnd();

				for (; it != end; ++it) {
					if (it.key() == constTitle) {
						continue;
					}
					if (tagMap.contains(it.key())) {
						if (it.key().startsWith(constAudio)) {
							audioProperties.insert(tagMap[it.key()].val, createRow(tagMap[it.key()].str, it.value()));
						}
						else {
							tags.insert(tagMap[it.key()].val, createRow(tagMap[it.key()].str, it.value()));
						}
					}
					else if (tagTimeMap.contains(it.key())) {
						tags.insert(tagTimeMap[it.key()].val, createRow(tagTimeMap[it.key()].str, QString(it.value()).replace("T", " ")));
					}
					else if (it.key().startsWith(constPerformer)) {
						tags.insert(3, createRow(tr("Performer (%1)").arg(clean(it.key().mid(constPerformer.length()))), it.value()));
					}
					else {
						tags.insert(tagMap.count() + tagTimeMap.count(), createRow(clean(it.key()), it.value()));
					}
				}
			}
		}
	}
#endif

	int audioPos = 1024;
	if (audioProperties.isEmpty()) {
		if (MPDStatus::self()->bitrate() > 0) {
			audioProperties.insert(audioPos++, createRow(tr("Bitrate"), tr("%1 kb/s").arg(MPDStatus::self()->bitrate())));
		}
		if (MPDStatus::self()->samplerate() > 0) {
			audioProperties.insert(audioPos++, createRow(tr("Sample rate"), tr("%1 Hz").arg(MPDStatus::self()->samplerate())));
		}
		if (MPDStatus::self()->channels() > 0) {
			audioProperties.insert(audioPos++, createRow(tr("Channels"), QString::number(MPDStatus::self()->channels())));
		}
	}
	if (MPDStatus::self()->bits() > 0) {
		audioProperties.insert(audioPos++, createRow(tr("Bits"), QString::number(MPDStatus::self()->bits())));
	}

	if (tags.isEmpty()) {
		int pos = 0;
		addIfNotEmpty(tags, pos++, tr("Artist"), currentSong.artist);
		addIfNotEmpty(tags, pos++, tr("Album artist"), currentSong.albumartist);
		addIfNotEmpty(tags, pos++, tr("Composer"), currentSong.composer());
		addIfNotEmpty(tags, pos++, tr("Performer"), currentSong.performer());
		addIfNotEmpty(tags, pos++, tr("Album"), currentSong.album);
		addIfNotEmpty(tags, pos++, tr("Track number"), 0 == currentSong.track ? QString() : QString::number(currentSong.track));
		addIfNotEmpty(tags, pos++, tr("Disc number"), 0 == currentSong.disc ? QString() : QString::number(currentSong.disc));
		addIfNotEmpty(tags, pos++, tr("Grouping"), currentSong.grouping());
		addIfNotEmpty(tags, pos++, tr("Duration"), 0 == currentSong.time ? QString() : Utils::formatTime(currentSong.time));
		addIfNotEmpty(tags, pos++, tr("Genre"), currentSong.displayGenre());
		addIfNotEmpty(tags, pos++, tr("Year"), 0 == currentSong.track ? QString() : QString::number(currentSong.year));
		addIfNotEmpty(tags, pos++, tr("Original Year"), 0 == currentSong.origYear ? QString() : QString::number(currentSong.origYear));
		addIfNotEmpty(tags, pos++, tr("Comment"), fixNewLine(currentSong.comment()));
	}

	QString tagInfo;
	if (!tags.isEmpty()) {
		tagInfo = QLatin1String("<table>");
		QMultiMap<int, QString>::ConstIterator it = tags.constBegin();
		QMultiMap<int, QString>::ConstIterator end = tags.constEnd();
		for (; it != end; ++it) {
			if (!it.value().isEmpty()) {
				tagInfo += it.value();
			}
		}
	}

	if (!audioProperties.isEmpty()) {
		if (tagInfo.isEmpty()) {
			tagInfo = QLatin1String("<table>");
		}
		else {
			tagInfo += QLatin1String("<tr/>");
		}
		QMultiMap<int, QString>::ConstIterator it = audioProperties.constBegin();
		QMultiMap<int, QString>::ConstIterator end = audioProperties.constEnd();
		for (; it != end; ++it) {
			if (!it.value().isEmpty()) {
				tagInfo += it.value();
			}
		}
	}
	if (tagInfo.isEmpty()) {
		tagInfo = QLatin1String("<table>");
	}
	else {
		tagInfo += QLatin1String("<tr/>");
	}
	QString songFile = currentSong.filePath(MPDConnection::self()->getDetails().dir);
	if (QFile::exists(songFile)) {
		tagInfo += createRow(tr("Filename"), QLatin1String("<a href=\"file://") + songFile + QLatin1String("\">") + songFile.toHtmlEscaped() + QLatin1String("</a>"), false);
	}
	else {
		tagInfo += createRow(tr("Filename"), songFile);
	}
	tagInfo += QLatin1String("</table>");

	setHtml(tagInfo, Page_Metadata);
}

void SongView::refreshInfo()
{
	if (currentSong.isEmpty()) {
		return;
	}
	for (const QString& lang : engine->getLangs()) {
		QFile::remove(infoCacheFileName(currentSong, engine->getPrefix(lang), false));
	}
	searchForInfo();
}

void SongView::searchForInfo()
{
	cancelInfoJobAction->setEnabled(true);
	engine->search(QStringList() << currentSong.basicArtist() << currentSong.basicTitle(), ContextEngine::Track);
	showSpinner(false);
}

void SongView::infoSearchResponse(const QString& resp, const QString& lang)
{
	cancelInfoJobAction->setEnabled(false);
	hideSpinner();
	QString str;
	if (!resp.isEmpty()) {
		str = engine->translateLinks(resp);
		if (!lang.isEmpty()) {
			QFile f(infoCacheFileName(currentSong, lang, true));
			QtIOCompressor compressor(&f);
			compressor.setStreamFormat(QtIOCompressor::GzipFormat);
			if (compressor.open(QIODevice::WriteOnly)) {
				compressor.write(resp.toUtf8().constData());
			}
		}
	}
	setHtml(str, Page_Information);
}

void SongView::abortInfoSearch()
{
	if (cancelInfoJobAction->isEnabled()) {
		cancelInfoJobAction->setEnabled(false);
		engine->cancel();
		hideSpinner();
	}
}

void SongView::showMoreInfo(const QUrl& url)
{
	QDesktopServices::openUrl(url);
}

void SongView::hideSpinner()
{
	if (!cancelInfoJobAction->isEnabled() && !cancelJobAction->isEnabled()) {
		View::hideSpinner();
	}
}

void SongView::abort()
{
	if (job) {
		job->cancelAndDelete();
		job = nullptr;
	}
	currentProvider = -1;
	if (currentProv) {
		currentProv->abort();
		currentProv = nullptr;

		text->setText(QString());
		// Set lyrics file anyway - so that editing is enabled!
		lyricsFile = Settings::self()->storeLyricsInMpdDir() && !currentSong.isNonMPD()
				? mpdLyricsFilePath(currentSong)
				: lyricsCacheFileName(currentSong);
		setMode(Mode_Display);
	}
	cancelJobAction->setEnabled(false);
	hideSpinner();
}

void SongView::update(const Song& s, bool force)
{
	if (s.isEmpty()) {
		currentSong = s;
		infoNeedsUpdating = metadataNeedsUpdating = lyricsNeedsUpdating = false;
		clear();
		abort();
		return;
	}

	Song song(s);
	bool songChanged = song.artist != currentSong.artist || song.title != currentSong.title;

	if (songChanged) {
		abort();
	}
	if (!isVisible()) {
		if (songChanged) {
			needToUpdate = true;
		}
		currentSong = song;
		return;
	}

	if (force || songChanged) {
		setMode(Mode_Blank);
		//        controls->setVisible(true);

		if (song.title.isEmpty() || song.artist.isEmpty()) {
			clear();
			infoNeedsUpdating = lyricsNeedsUpdating = false;
		}
		else {
			currentRequest++;
			infoNeedsUpdating = lyricsNeedsUpdating = true;
		}

		// Only reset the provider if the refresh was an automatic one or if the song has
		// changed. Otherwise we'll keep the provider so the user can cycle through the lyrics
		// offered by the various providers.
		if (!force || songChanged) {
			currentProvider = -1;
		}
	}

	metadataNeedsUpdating = force || song.isDifferent(currentSong);
	currentSong = song;

	if (!song.title.isEmpty()) {
		setHeader(song.title);
	}
	if (metadataNeedsUpdating || infoNeedsUpdating || lyricsNeedsUpdating) {
		curentViewChanged();
	}
}

void SongView::downloadFinished()
{
	NetworkJob* reply = qobject_cast<NetworkJob*>(sender());
	if (reply) {
		reply->deleteLater();
		if (job == reply) {
			job = nullptr;
			if (reply->ok()) {
				QString file = reply->property("file").toString();
				if (!file.isEmpty() && file == currentSong.file) {
					QTextStream str(reply->actualJob());
					QString lyrics = str.readAll();
					if (!lyrics.isEmpty()) {
						text->setText(fixNewLines(lyrics));
						cancelJobAction->setEnabled(false);
						hideSpinner();
						return;
					}
				}
			}
		}
	}
	loadLyricsFromFile();
}

void SongView::lyricsReady(int id, QString lyrics)
{
	if (id != currentRequest) {
		return;
	}
	lyrics = lyrics.trimmed();

	if (lyrics.isEmpty()) {
		getLyrics();
	}
	else {
		cancelJobAction->setEnabled(false);
		hideSpinner();
		QString before = text->toHtml();
		text->setText(fixNewLines(lyrics));
		// Remove formatting, as we dont save this anyway...
		QString plain = text->toPlainText().trimmed();

		if (plain.isEmpty()) {
			text->setText(before);
			getLyrics();
		}
		else {
			text->setText(fixNewLines(plain));
			lyricsFile = QString();
			if (!(Settings::self()->storeLyricsInMpdDir() && !currentSong.isNonMPD() && saveFile(mpdLyricsFilePath(currentSong)))) {
				saveFile(lyricsCacheFileName(currentSong, true));
			}
			setMode(Mode_Display);
		}
	}
}

bool SongView::saveFile(const QString& fileName)
{
	QFile f(fileName);

	if (f.open(QIODevice::WriteOnly)) {
		QTextStream stream(&f);
#ifdef Q_OS_WIN
		stream.setEncoding(QStringConverter::Utf8);
		stream.setGenerateByteOrderMark(true);
#endif
		stream << text->toPlainText();
		f.close();
		lyricsFile = fileName;
		return true;
	}

	return false;
}

QString SongView::mpdFileName() const
{
	return currentSong.file.isEmpty() || MPDConnection::self()->getDetails().dir.isEmpty() || currentSong.isNonMPD()
			? QString()
			: mpdLyricsFilePath(currentSong);
}

QString SongView::cacheFileName() const
{
	return currentSong.artist.isEmpty() || currentSong.title.isEmpty() ? QString() : lyricsCacheFileName(currentSong);
}

void SongView::getLyrics()
{
	currentProv = UltimateLyrics::self()->getNext(currentProvider);
	if (currentProv) {
		text->setText(tr("Fetching lyrics via %1").arg(currentProv->displayName()));
		currentProv->fetchInfo(currentRequest, currentSong);
		showSpinner();
	}
	else {
		text->setText(QString());
		currentProvider = -1;
		// Set lyrics file anyway - so that editing is enabled!
		lyricsFile = Settings::self()->storeLyricsInMpdDir() && !currentSong.isNonMPD()
				? mpdLyricsFilePath(currentSong)
				: lyricsCacheFileName(currentSong);
		setMode(Mode_Display);
	}
}

void SongView::setMode(Mode m)
{
	if (Mode_Display == m) {
		cancelJobAction->setEnabled(false);
		hideSpinner();
	}
	if (mode == m) {
		return;
	}
	mode = m;
	bool fileExists = Mode_Display == m && !lyricsFile.isEmpty() && QFileInfo(lyricsFile).isWritable();
	delAction->setEnabled(fileExists);
	if (scrollAction->isChecked()) {
		if (Mode_Display == mode) {
			scroll();
		}
		else if (scrollTimer) {
			scrollTimer->stop();
		}
	}
}

bool SongView::setLyricsFromFile(const QString& filePath)
{
	QFile f(filePath);

	if (f.exists() && f.open(QIODevice::ReadOnly)) {
		// Read the file using a QTextStream so we get automatic UTF8 detection.
		QTextStream inputStream(&f);
		text->setText(fixNewLines(inputStream.readAll()));
		cancelJobAction->setEnabled(false);
		hideSpinner();
		f.close();

		return true;
	}

	return false;
}

#include "moc_songview.cpp"
