/*
 * 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 "rgdialog.h"
#ifdef ENABLE_DEVICES_SUPPORT
#include "devices/device.h"
#include "models/devicesmodel.h"
#endif
#include "jobcontroller.h"
#include "mpd-interface/cuefile.h"
#include "mpd-interface/mpdconnection.h"
#include "support/action.h"
#include "support/icon.h"
#include "support/messagebox.h"
#include "support/utils.h"
#include "tagreader.h"
#include "tags/tags.h"
#include "widgets/basicitemdelegate.h"
#include <QBoxLayout>
#include <QCloseEvent>
#include <QComboBox>
#include <QCoreApplication>
#include <QEventLoop>
#include <QHeaderView>
#include <QLabel>
#include <QProgressBar>
#include <QTreeWidget>
#include <algorithm>

enum Columns {
	COL_ARTIST,
	COL_ALBUM,
	COL_TITLE,
	COL_ALBUMGAIN,
	COL_TRACKGAIN,
	COL_ALBUMPEAK,
	COL_TRACKPEAK
};

static int iCount = 0;

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

static inline void setResizeMode(QHeaderView* hdr, int idx, QHeaderView::ResizeMode mode)
{
	hdr->setSectionResizeMode(idx, mode);
}

RgDialog::RgDialog(QWidget* parent)
	: SongDialog(parent, "RgDialog", QSize(800, 400)), state(State_Idle), totalToScan(0), tagReader(0), autoScanTags(false)
{
	iCount++;
	setButtons(User1 | Ok | Cancel);
	setCaption(tr("ReplayGain"));
	setAttribute(Qt::WA_DeleteOnClose);
	QWidget* mainWidget = new QWidget(this);
	QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, mainWidget);
	combo = new QComboBox(this);
	view = new QTreeWidget(this);
	statusLabel = new QLabel(this);
	statusLabel->setVisible(false);
	progress = new QProgressBar(this);
	progress->setVisible(false);
	combo->addItem(tr("Show All Tracks"), true);
	combo->addItem(tr("Show Untagged Tracks"), false);
	view->setRootIsDecorated(false);
	view->setAllColumnsShowFocus(true);
	view->setItemDelegate(new BasicItemDelegate(view));
	view->setAlternatingRowColors(false);
	view->setContextMenuPolicy(Qt::ActionsContextMenu);
	view->setSelectionMode(QAbstractItemView::ExtendedSelection);
	removeAct = new Action(tr("Remove From List"), view);
	removeAct->setEnabled(false);
	view->addAction(removeAct);
	QTreeWidgetItem* hdr = view->headerItem();
	hdr->setText(COL_ARTIST, tr("Artist"));
	hdr->setText(COL_ALBUM, tr("Album"));
	hdr->setText(COL_TITLE, tr("Title"));
	hdr->setText(COL_ALBUMGAIN, tr("Album Gain"));
	hdr->setText(COL_TRACKGAIN, tr("Track Gain"));
	hdr->setText(COL_ALBUMPEAK, tr("Album Peak"));
	hdr->setText(COL_TRACKPEAK, tr("Track Peak"));

	QHeaderView* hv = view->header();
	setResizeMode(hv, COL_ARTIST, QHeaderView::ResizeToContents);
	setResizeMode(hv, COL_ALBUM, QHeaderView::ResizeToContents);
	setResizeMode(hv, COL_TITLE, QHeaderView::Stretch);
	setResizeMode(hv, COL_ALBUMGAIN, QHeaderView::Fixed);
	setResizeMode(hv, COL_TRACKGAIN, QHeaderView::Fixed);
	setResizeMode(hv, COL_ALBUMPEAK, QHeaderView::Fixed);
	setResizeMode(hv, COL_TRACKPEAK, QHeaderView::Fixed);
	hv->setStretchLastSection(false);

	layout->setContentsMargins(0, 0, 0, 0);
	layout->addWidget(combo);
	layout->addWidget(view);
	layout->addWidget(statusLabel);
	layout->addWidget(progress);
	setMainWidget(mainWidget);
	setButtonGuiItem(Ok, StdGuiItem::save());
	setButtonGuiItem(Cancel, StdGuiItem::close());
	setButtonGuiItem(User1, GuiItem(tr("Scan"), fa::fa_solid, fa::fa_search));
	enableButton(Ok, false);
	enableButton(User1, false);
	qRegisterMetaType<Tags::ReplayGain>("Tags::ReplayGain");
	connect(combo, SIGNAL(currentIndexChanged(int)), SLOT(toggleDisplay()));
	connect(view, SIGNAL(itemSelectionChanged()), SLOT(controlRemoveAct()));
	connect(removeAct, SIGNAL(triggered()), SLOT(removeItems()));

	italic = font();
	italic.setItalic(true);
	JobController::self()->setMaxActive(1);
}

RgDialog::~RgDialog()
{
	clearScanners();
	iCount--;
}

void RgDialog::show(const QList<Song>& songs, const QString& udi, bool autoScan)
{
	if (songs.isEmpty()) {
		deleteLater();
		return;
	}

	for (const Song& s : songs) {
		if (!CueFile::isCue(s.file)) {
			origSongs.append(s);
		}
	}

	if (origSongs.isEmpty()) {
		deleteLater();
		return;
	}

	autoScanTags = autoScan;
	std::sort(origSongs.begin(), origSongs.end());

#ifdef ENABLE_DEVICES_SUPPORT
	if (udi.isEmpty()) {
		base = MPDConnection::self()->getDetails().dir;
	}
	else {
		Device* dev = getDevice(udi, parentWidget());

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

		base = dev->path();
	}
#else
	base = MPDConnection::self()->getDetails().dir;
#endif

	if (!songsOk(origSongs, base, udi.isEmpty())) {
		return;
	}

	state = State_Idle;
	enableButton(User1, origSongs.count());
	view->clear();
	for (const Song& s : origSongs) {
		if (s.isLocalFile()) {
			base = QString();
		}
		QString aa = s.albumArtist();
		const QString& al = s.album;
		const QString& t = s.title;
		if (aa.isEmpty() && al.isEmpty() && t.isEmpty()) {
			new QTreeWidgetItem(view, QStringList() << "-"
			                                        << "-" << Utils::getFile(s.file));
		}
		else {
			new QTreeWidgetItem(view, QStringList() << s.albumArtist() << s.album << s.title);
		}
	}
	Dialog::show();
	startReadingTags();
}

void RgDialog::slotButtonClicked(int button)
{
	if (State_Saving == state) {
		return;
	}

	switch (button) {
	case Ok:
		//if (MessageBox::Yes==MessageBox::questionYesNo(this, tr("Update ReplayGain tags in tracks?"), tr("Update Tags"),
		//                                               GuiItem(tr("Update Tags")), StdGuiItem::cancel())) {
		if (saveTags()) {
			stopScanning();
			accept();
		}
		//}
		break;
	case User1:
		startScanning();
		break;
	case Cancel:
		switch (state) {
		case State_ScanningFiles:
			if (MessageBox::Yes == MessageBox::questionYesNo(this, tr("Abort scanning of tracks?"), tr("Abort"), GuiItem(tr("Abort")), StdGuiItem::no())) {
				stopScanning();
				// Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
				Dialog::slotButtonClicked(button);
				state = State_Idle;
			}
			break;
		case State_ScanningTags:
			if (MessageBox::Yes == MessageBox::questionYesNo(this, tr("Abort reading of existing tags?"), tr("Abort"), GuiItem(tr("Abort")), StdGuiItem::no())) {
				stopReadingTags();
				// Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
				Dialog::slotButtonClicked(button);
				state = State_Idle;
			}
			break;
		default:
		case State_Idle:
			stopScanning();
			reject();
			// Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
			Dialog::slotButtonClicked(button);
			break;
		}
		break;
	default:
		break;
	}
}

void RgDialog::startScanning()
{
	bool all = origTags.isEmpty() || (origTags.count() == origSongs.count() ? MessageBox::Yes == MessageBox::questionYesNo(this, tr("Scan <b>all</b> tracks?<br/><br/><i>All tracks have existing ReplayGain tags.</i>"), QString(), GuiItem(tr("Scan")), StdGuiItem::cancel()) : MessageBox::No == MessageBox::questionYesNo(this, tr("Do you wish to scan all tracks, or only tracks without existing tags?"), QString(), GuiItem(tr("Untagged Tracks")), GuiItem(tr("All Tracks"))));
	if (!all && origTags.count() == origSongs.count()) {
		return;
	}
	setButtonGuiItem(Cancel, StdGuiItem::cancel());
	state = State_ScanningFiles;
	enableButton(Ok, false);
	enableButton(User1, false);
	progress->setValue(0);
	progress->setVisible(true);
	statusLabel->setText(tr("Scanning tracks..."));
	statusLabel->setVisible(true);
	clearScanners();
	totalToScan = 0;
	QMap<QString, QList<int>> groupedTracks;
	for (int i = 0; i < origSongs.count(); ++i) {
		if (!removedItems.contains(i) && (all || !origTags.contains(i))) {
			const Song& sng = origSongs.at(i);
			QString aa = sng.albumArtist();
			const QString& al = sng.album;
			groupedTracks[aa.isEmpty() && al.isEmpty() ? (Utils::getFile(sng.file) + " -- " + QString::number(i)) : (sng.albumArtist() + " -- " + sng.album)].append(i);
		}
	}
	QMap<QString, QList<int>>::ConstIterator it(groupedTracks.constBegin());
	QMap<QString, QList<int>>::ConstIterator end(groupedTracks.constEnd());

	for (; it != end; ++it) {
		createScanner(*it);
		totalToScan++;
	}
	progress->setRange(0, 100 * totalToScan);
}

void RgDialog::stopScanning()
{
	state = State_Idle;
	enableButton(User1, true);
	progress->setVisible(false);
	statusLabel->setVisible(false);

	JobController::self()->cancel();
	clearScanners();
	setButtonGuiItem(Cancel, StdGuiItem::close());
}

void RgDialog::createScanner(const QList<int>& indexes)
{
	QMap<int, QString> fileMap;
	for (int i : indexes) {
		fileMap[i] = origSongs.at(i).filePath(base);
	}

	AlbumScanner* s = new AlbumScanner(fileMap);
	connect(s, SIGNAL(progress(int)), this, SLOT(scannerProgress(int)));
	connect(s, SIGNAL(done()), this, SLOT(scannerDone()));
	scanners[s] = 0;
	JobController::self()->add(s);
}

void RgDialog::clearScanners()
{
	QList<AlbumScanner*> scannerList = scanners.keys();
	for (AlbumScanner* sc : scannerList) {
		sc->stop();
	}
	scanners.clear();
}

void RgDialog::startReadingTags()
{
	if (tagReader) {
		return;
	}
	setButtonGuiItem(Cancel, StdGuiItem::cancel());
	state = State_ScanningTags;
	enableButton(Ok, false);
	enableButton(User1, false);
	progress->setRange(0, origSongs.count());
	progress->setVisible(true);
	statusLabel->setText(tr("Reading existing tags..."));
	statusLabel->setVisible(true);
	tagReader = new TagReader();
	tagReader->setDetails(origSongs, base);
	connect(tagReader, SIGNAL(progress(int, Tags::ReplayGain)), this, SLOT(songTags(int, Tags::ReplayGain)));
	connect(tagReader, SIGNAL(done()), this, SLOT(tagReaderDone()));
	JobController::self()->add(tagReader);
}

void RgDialog::stopReadingTags()
{
	if (!tagReader) {
		return;
	}

	state = State_Idle;
	enableButton(User1, true);
	progress->setVisible(false);
	statusLabel->setVisible(false);
	disconnect(tagReader, SIGNAL(progress(int, Tags::ReplayGain)), this, SLOT(songTags(int, Tags::ReplayGain)));
	disconnect(tagReader, SIGNAL(done()), this, SLOT(tagReaderDone()));
	tagReader->requestAbort();
	tagReader = 0;
	autoScanTags = false;
}

bool RgDialog::saveTags()
{
	state = State_Saving;
	enableButton(Ok, false);
	enableButton(Close, false);
	enableButton(Cancel, false);
	enableButton(User1, false);

	QStringList failed;

	progress->setVisible(true);
	progress->setRange(0, tagsToSave.count());

	int count = 0;
	bool someTimedout = false;
	QMap<int, Tags::ReplayGain>::ConstIterator it = tagsToSave.constBegin();
	QMap<int, Tags::ReplayGain>::ConstIterator end = tagsToSave.constEnd();

	for (; it != end; ++it) {
		QString filePath = origSongs.at(it.key()).filePath(base);
		switch (Tags::updateReplaygain(filePath, it.value())) {
		case Tags::Update_Failed:
			failed.append(filePath);
			break;
		case Tags::Update_BadFile:
			failed.append(tr("%1 (Corrupt tags?)", "filename (Corrupt tags?)").arg(filePath));
			break;
		default:
			break;
		}

		progress->setValue(progress->value() + 1);
		if (0 == count++ % 10) {
			QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
		}
	}

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

	return !someTimedout;
}

void RgDialog::updateView()
{
	int finished = 0;
	quint64 totalProgress = 0;
	QMap<AlbumScanner*, int>::ConstIterator it = scanners.constBegin();
	QMap<AlbumScanner*, int>::ConstIterator end = scanners.constEnd();

	for (; it != end; ++it) {
		if (100 == it.value()) {
			finished++;
		}
		totalProgress += (*it);
	}

	if (finished == totalToScan) {
		progress->setVisible(false);
		statusLabel->setVisible(false);
		state = State_Idle;
		setButtonGuiItem(Cancel, StdGuiItem::close());
		enableButton(Ok, !tagsToSave.isEmpty());
	}
	else {
		progress->setValue(totalProgress);
	}
}

#ifdef ENABLE_DEVICES_SUPPORT
Device* RgDialog::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 0;
	}
	if (!dev->isConnected()) {
		MessageBox::error(p ? p : this, tr("Device is not connected."));
		reject();
		return 0;
	}
	if (!dev->isIdle()) {
		MessageBox::error(p ? p : this, tr("Device is busy?"));
		reject();
		return 0;
	}
	return dev;
}
#endif

void RgDialog::scannerProgress(int p)
{
	AlbumScanner* s = qobject_cast<AlbumScanner*>(sender());
	if (!s) {
		return;
	}

	scanners[s] = p;
	updateView();
}

void RgDialog::scannerDone()
{
	AlbumScanner* s = qobject_cast<AlbumScanner*>(sender());
	if (!s) {
		return;
	}

	const QMap<int, AlbumScanner::Values>& trackValues = s->trackValues();
	QMap<int, AlbumScanner::Values>::ConstIterator it(trackValues.constBegin());
	QMap<int, AlbumScanner::Values>::ConstIterator end(trackValues.constEnd());

	if (s->success()) {
		for (; it != end; ++it) {
			Tags::ReplayGain updatedTags(it.value().gain, s->albumValues().gain, it.value().peak, s->albumValues().peak);
			QTreeWidgetItem* item = view->topLevelItem(it.key());
			if (it.value().ok) {
				item->setText(COL_TRACKGAIN, tr("%1 dB").arg(Utils::formatNumber(updatedTags.trackGain, 2)));
				if (!Utils::equal(updatedTags.trackPeak, 0.0)) {
					item->setText(COL_TRACKPEAK, Utils::formatNumber(updatedTags.trackPeak, 6));
				}
			}
			else {
				item->setText(COL_TRACKGAIN, tr("Failed"));
				item->setText(COL_TRACKPEAK, tr("Failed"));
			}
			if (s->albumValues().ok) {
				item->setText(COL_ALBUMGAIN, tr("%1 dB").arg(Utils::formatNumber(updatedTags.albumGain, 2)));
				if (!Utils::equal(updatedTags.albumPeak, 0.0)) {
					item->setText(COL_ALBUMPEAK, Utils::formatNumber(updatedTags.albumPeak, 6));
				}
			}
			else {
				item->setText(COL_ALBUMGAIN, tr("Failed"));
				item->setText(COL_ALBUMPEAK, tr("Failed"));
			}

			if (it.value().ok && origTags.contains(it.key())) {
				Tags::ReplayGain t = origTags[it.key()];
				bool diff = false;
				bool diffAlbum = false;
				if (!Utils::equal(t.trackGain, updatedTags.trackGain, 0.01)) {
					item->setFont(COL_TRACKGAIN, italic);
					item->setToolTip(COL_TRACKGAIN, tr("Original: %1 dB").arg(Utils::formatNumber(t.trackGain, 2)));
					diff = true;
				}
				if (!Utils::equal(updatedTags.trackPeak, 0.0) && !Utils::equal(t.trackPeak, updatedTags.trackPeak, 0.000001)) {
					item->setFont(COL_TRACKPEAK, italic);
					item->setToolTip(COL_TRACKPEAK, tr("Original: %1").arg(Utils::formatNumber(t.trackPeak, 6)));
					diff = true;
				}
				if (!Utils::equal(t.albumGain, updatedTags.albumGain, 0.01)) {
					item->setFont(COL_ALBUMGAIN, italic);
					item->setToolTip(COL_ALBUMGAIN, tr("Original: %1 dB").arg(Utils::formatNumber(t.albumGain, 2)));
					diffAlbum = true;
				}
				if (!Utils::equal(updatedTags.albumPeak, 0.0) && !Utils::equal(t.albumPeak, updatedTags.albumPeak, 0.000001)) {
					item->setFont(COL_ALBUMPEAK, italic);
					item->setToolTip(COL_ALBUMPEAK, tr("Original: %1").arg(Utils::formatNumber(t.albumPeak, 6)));
					diffAlbum = true;
				}
				if (diff || diffAlbum) {
					if (diff) {
						item->setFont(COL_ARTIST, italic);
						item->setFont(COL_TITLE, italic);
					}
					if (diffAlbum) {
						item->setFont(COL_ALBUM, italic);
					}
					tagsToSave.insert(it.key(), updatedTags);
				}
				else {
					tagsToSave.remove(it.key());
				}
			}
			else if (it.value().ok) {
				item->setFont(COL_ARTIST, italic);
				item->setFont(COL_ALBUM, italic);
				item->setFont(COL_TITLE, italic);
				item->setFont(COL_TRACKGAIN, italic);
				item->setFont(COL_TRACKPEAK, italic);
				item->setFont(COL_ALBUMGAIN, italic);
				item->setFont(COL_ALBUMPEAK, italic);
				tagsToSave.insert(it.key(), updatedTags);
			}
			else {
				tagsToSave.remove(it.key());
			}
		}
	}
	else {
		for (; it != end; ++it) {
			QTreeWidgetItem* item = view->topLevelItem(it.key());
			item->setText(COL_TRACKGAIN, tr("Failed"));
			item->setText(COL_TRACKPEAK, tr("Failed"));
			item->setText(COL_ALBUMGAIN, tr("Failed"));
			item->setText(COL_ALBUMPEAK, tr("Failed"));
			tagsToSave.remove(it.key());
		}
	}

	scanners[s] = 100;
	updateView();
	JobController::self()->finishedWith(s);
}

void RgDialog::songTags(int index, Tags::ReplayGain tags)
{
	if (index >= 0 && index < origSongs.count()) {
		progress->setValue(progress->value() + 1);
		if (!tags.isEmpty()) {
			origTags[index] = tags;

			QTreeWidgetItem* item = view->topLevelItem(index);
			if (!item) {
				return;
			}
			item->setText(COL_TRACKGAIN, tr("%1 dB").arg(Utils::formatNumber(tags.trackGain, 2)));
			if (!Utils::equal(tags.trackPeak, 0.0)) {
				item->setText(COL_TRACKPEAK, Utils::formatNumber(tags.trackPeak, 6));
			}
			item->setText(COL_ALBUMGAIN, tr("%1 dB").arg(Utils::formatNumber(tags.albumGain, 2)));
			if (!Utils::equal(tags.albumPeak, 0.0)) {
				item->setText(COL_ALBUMPEAK, Utils::formatNumber(tags.albumPeak, 6));
			}
		}
	}
}

void RgDialog::tagReaderDone()
{
	TagReader* t = qobject_cast<TagReader*>(sender());
	if (!t) {
		return;
	}

	JobController::self()->finishedWith(t);
	tagReader = 0;

	state = State_Idle;
	enableButton(User1, true);
	progress->setVisible(false);
	statusLabel->setVisible(false);

	if (autoScanTags) {
		autoScanTags = false;
		startScanning();
	}
}

void RgDialog::toggleDisplay()
{
	bool showAll = combo->itemData(combo->currentIndex()).toBool();

	for (int i = 0; i < view->topLevelItemCount(); ++i) {
		QTreeWidgetItem* item = view->topLevelItem(i);
		if (!removedItems.contains(view->indexOfTopLevelItem(item))) {
			item->setHidden(showAll ? false : origTags.contains(i));
		}
	}
}

void RgDialog::controlRemoveAct()
{
	removeAct->setEnabled(view->topLevelItemCount() > 1 && !view->selectedItems().isEmpty());
}

void RgDialog::removeItems()
{
	if (view->topLevelItemCount() < 1) {
		return;
	}

	if (MessageBox::Yes == MessageBox::questionYesNo(this, tr("Remove the selected tracks from the list?"), tr("Remove Tracks"), StdGuiItem::remove(), StdGuiItem::cancel())) {
		QList<QTreeWidgetItem*> selection = view->selectedItems();
		for (QTreeWidgetItem* item : selection) {
			int index = view->indexOfTopLevelItem(item);
			item->setHidden(true);
			removedItems.insert(index);
			if (tagsToSave.contains(index)) {
				tagsToSave.remove(index);
			}
		}
	}
}

void RgDialog::closeEvent(QCloseEvent* event)
{
	if (State_Idle != state) {
		slotButtonClicked(Cancel);
		if (State_Idle != state) {
			event->ignore();
		}
	}
	else {
		Dialog::closeEvent(event);
	}
}

#include "moc_rgdialog.cpp"
