/*
 * Cantata
 *
 * Copyright (c) 2011-2022 Craig Drummond <craig.p.drummond@gmail.com>
 *
 */
/*
  Copyright (C) 2005-2007 Richard Lärkäng <nouseforaname@home.se>

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Library General Public
  License as published by the Free Software Foundation; either
  version 2 of the License, or (at your option) any later version.

  This library 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
  Library General Public License for more details.

  You should have received a copy of the GNU Library General Public License
    along with this library; see the file COPYING.LIB.  If not, write to
  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  Boston, MA 02110-1301, USA.
*/

#include "musicbrainz.h"
#include "config.h"
#include "network/networkproxyfactory.h"
#include "support/thread.h"
#include <Artist.h>
#include <ArtistCredit.h>
#include <Disc.h>
#include <HTTPFetch.h>
#include <Medium.h>
#include <NameCredit.h>
#include <QCryptographicHash>
#include <QList>
#include <QNetworkProxy>
#include <QRegularExpression>
#include <Query.h>
#include <Recording.h>
#include <Release.h>
#include <ReleaseGroup.h>
#include <Track.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
#include <arpa/inet.h>
#include <sys/cdio.h>
#elif defined(__linux__)
#include <linux/cdrom.h>
#endif

#include <QDebug>
#define DBUG qDebug()

static const int constFramesPerSecond = 75;
static const int constDataTrackAdjust = 11400;

static inline int secondsToFrames(int s)
{
	return constFramesPerSecond * s;
}

static inline int framesToSeconds(int f)
{
	return (f / (constFramesPerSecond * 1.0)) + 0.5;
}

struct Track {
	Track(int o = 0, bool d = false) : offset(o), isData(d) {}
	int offset;
	bool isData;
};

static QString calculateDiscId(const QList<Track>& tracks)
{
	if (tracks.isEmpty()) {
		return QString();
	}

	// Code based on libmusicbrainz/lib/diskid.cpp
	int numTracks = tracks.count() - 1;
	QCryptographicHash sha(QCryptographicHash::Sha1);
	QString temp;

	temp = QStringLiteral("%1").arg(1, 2, 16, QLatin1Char('0'));
	sha.addData(temp.toUpper().toLatin1());
	temp = QStringLiteral("%1").arg(numTracks, 2, 16, QLatin1Char('0'));
	sha.addData(temp.toUpper().toLatin1());

	for (int i = 0; i < 100; i++) {
		int offset;
		if (0 == i) {
			offset = tracks[numTracks].offset;
		}
		else if (i <= numTracks) {
			offset = tracks[i - 1].offset;
		}
		else {
			offset = 0;
		}

		temp = QStringLiteral("%1").arg(offset, 8, 16, QLatin1Char('0'));
		sha.addData(temp.toUpper().toLatin1());
	}

	QByteArray base64 = sha.result().toBase64();
	// '/' '+' and '=' replaced for MusicBrainz
	return QString::fromLatin1(base64).replace(QLatin1Char('/'), QLatin1String("_")).replace(QLatin1Char('+'), QLatin1String(".")).replace(QLatin1Char('='), QLatin1String("-"));
}

static QString artistFromCreditList(MusicBrainz5::CArtistCredit* artistCredit)
{
	QString artistName;
	MusicBrainz5::CNameCreditList* artistList = artistCredit->NameCreditList();

	if (artistList) {
		for (int i = 0; i < artistList->NumItems(); i++) {
			MusicBrainz5::CNameCredit* name = artistList->Item(i);
			MusicBrainz5::CArtist* artist = name->Artist();

			if (!name->Name().empty()) {
				artistName += QString::fromUtf8(name->Name().c_str());
			}
			else {
				artistName += QString::fromUtf8(artist->Name().c_str());
			}
			artistName += QString::fromUtf8(name->JoinPhrase().c_str());
		}
	}

	return artistName;
}

MusicBrainz::MusicBrainz(const QString& device)
	: dev(device)
{
	thread = new Thread(metaObject()->className());
	moveToThread(thread);
	thread->start();
}

MusicBrainz::~MusicBrainz()
{
	thread->stop();
}

void MusicBrainz::readDisc()
{
	int fd = open(dev.toLocal8Bit(), O_RDONLY | O_NONBLOCK);
	if (fd < 0) {
		emit error(tr("Failed to open CD device"));
		return;
	}
	QList<Track> tracks;

#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
	struct ioc_toc_header th;
	struct ioc_read_toc_single_entry te;
	struct ioc_read_subchannel cdsc;
	struct cd_sub_channel_info data;
	bzero(&cdsc, sizeof(cdsc));
	cdsc.data = &data;
	cdsc.data_len = sizeof(data);
	cdsc.data_format = CD_CURRENT_POSITION;
	cdsc.address_format = CD_MSF_FORMAT;
	if (ioctl(fd, CDIOCREADSUBCHANNEL, (char*)&cdsc) >= 0 && 0 == ioctl(fd, CDIOREADTOCHEADER, &th)) {
		te.address_format = CD_LBA_FORMAT;
		for (int i = th.starting_track; i <= th.ending_track; i++) {
			te.track = i;
			if (0 == ioctl(fd, CDIOREADTOCENTRY, &te)) {
				tracks.append(Track(te.entry.addr.lba + secondsToFrames(2), te.entry.control & 0x04));
			}
		}
		te.track = 0xAA;
		if (0 == ioctl(fd, CDIOREADTOCENTRY, &te)) {
			tracks.append((ntohl(te.entry.addr.lba) + secondsToFrames(2)) / secondsToFrames(1));
		}
	}
#elif defined(__linux__)
	struct cdrom_tochdr th;
	struct cdrom_tocentry te;
	int status = ioctl(fd, CDROM_DISC_STATUS, CDSL_CURRENT);
	if ((CDS_AUDIO == status || CDS_MIXED == status) && 0 == ioctl(fd, CDROMREADTOCHDR, &th)) {
		te.cdte_format = CDROM_LBA;
		for (int i = th.cdth_trk0; i <= th.cdth_trk1; i++) {
			te.cdte_track = i;
			if (0 == ioctl(fd, CDROMREADTOCENTRY, &te)) {
				tracks.append(Track(te.cdte_addr.lba + secondsToFrames(2), te.cdte_ctrl & CDROM_DATA_TRACK));
			}
		}
		te.cdte_track = CDROM_LEADOUT;
		if (0 == ioctl(fd, CDROMREADTOCENTRY, &te)) {
			tracks.append((te.cdte_addr.lba + secondsToFrames(2)));
		}
	}
#endif
	close(fd);

	initial.name = Song::unknown();
	initial.artist = Song::unknown();
	initial.genre = Song::unknown();
	initial.isDefault = true;

	if (tracks.count() > 1) {
		for (int i = 0; i < tracks.count() - 1; ++i) {
			const Track& trk = tracks.at(i);
			if (trk.isData) {
				continue;
			}
			const Track& next = tracks.at(i + 1);
			Song s;
			s.track = i + 1;
			s.title = tr("Track %1").arg(s.track).toUtf8();
			s.artist = Song::unknown();
			s.albumartist = initial.artist;
			s.album = initial.name;
			s.id = s.track;

			s.time = framesToSeconds((next.offset - trk.offset) - (next.isData ? constDataTrackAdjust : 0));
			s.file = QString("%1.wav").arg(s.track);
			s.year = initial.year;
			initial.tracks.append(s);
		}

		if (tracks.count() >= 3 && tracks.at(tracks.count() - 2).isData) {
			tracks.takeLast();
			Track last = tracks.takeLast();
			last.offset -= constDataTrackAdjust;
			tracks.append(last);
		}
	}

	discId = calculateDiscId(tracks);
	emit initialDetails(initial);
}

void MusicBrainz::lookup(bool full)
{
	bool isInitial = discId.isEmpty();
	if (isInitial) {
		readDisc();
	}

	if (!full) {
		return;
	}
	DBUG << "Should lookup " << discId;

	MusicBrainz5::CQuery Query("cantata-" PACKAGE_VERSION_STRING);
	QList<CdAlbum> m;
	QList<QNetworkProxy> proxies = NetworkProxyFactory::self()->queryProxy(QNetworkProxyQuery(QUrl("http://musicbrainz.org")));
	for (const QNetworkProxy& p : proxies) {
		if (QNetworkProxy::HttpProxy == p.type() && 0 != p.port()) {
			Query.SetProxyHost(p.hostName().toLatin1().constData());
			Query.SetProxyPort(p.port());
			break;
		}
	}

	// Code adapted from libmusicbrainz/examples/cdlookup.cc

	try {
		MusicBrainz5::CMetadata Metadata = Query.Query("discid", discId.toLatin1().constData());

		if (Metadata.Disc() && Metadata.Disc()->ReleaseList()) {
			MusicBrainz5::CReleaseList* releaseList = Metadata.Disc()->ReleaseList();
			DBUG << "Found " << releaseList->NumItems() << " release(s)";

			for (int i = 0; i < releaseList->NumItems(); i++) {
				MusicBrainz5::CRelease* release = releaseList->Item(i);

				//The releases returned from LookupDiscID don't contain full information

				MusicBrainz5::CQuery::tParamMap params;
				params["inc"] = "artists labels recordings release-groups url-rels discids artist-credits";

				std::string releaseId = release->ID();
				MusicBrainz5::CMetadata Metadata2 = Query.Query("release", releaseId, "", params);

				if (Metadata2.Release()) {
					MusicBrainz5::CRelease* fullRelease = Metadata2.Release();

					//However, these releases will include information for all media in the release
					//So we need to filter out the only the media we want.
					MusicBrainz5::CMediumList mediaList = fullRelease->MediaMatchingDiscID(discId.toLatin1().constData());

					if (mediaList.NumItems() > 0) {
						DBUG << "Found " << mediaList.NumItems() << " media item(s)";

						for (int i = 0; i < mediaList.NumItems(); i++) {
							MusicBrainz5::CMedium* medium = mediaList.Item(i);

							/*DBUG << "Found media: '" << medium.Title() << "', position " << medium.Position();*/
							CdAlbum album;

							album.name = QString::fromUtf8(fullRelease->Title().c_str());

							if (fullRelease->MediumList()->NumItems() > 1) {
								album.name = tr("%1 (Disc %2)").arg(album.name).arg(medium->Position());
								album.disc = medium->Position();
							}
							album.artist = artistFromCreditList(fullRelease->ArtistCredit());
							album.genre = Song::unknown();

							QString date = QString::fromUtf8(fullRelease->Date().c_str());
							static const QRegularExpression yearRe("^(\\d{4,4})(-\\d{1,2}-\\d{1,2})?$");
							QRegularExpressionMatch yearMatch = yearRe.match(date);
							if (yearMatch.hasMatch()) {
								QString yearString = yearMatch.captured(1);
								bool ok;
								album.year = yearString.toInt(&ok);
								if (!ok) {
									album.year = 0;
								}
							}

							MusicBrainz5::CTrackList* trackList = medium->TrackList();
							if (trackList) {
								for (int i = 0; i < trackList->NumItems(); i++) {
									// Ensure we have the same number of tracks are read from disc!
									if (album.tracks.count() >= initial.tracks.count()) {
										break;
									}
									MusicBrainz5::CTrack* track = trackList->Item(i);
									MusicBrainz5::CRecording* recording = track->Recording();
									Song song;

									song.albumartist = album.artist;
									song.album = album.name;
									song.genres[0] = album.genre;
									song.id = song.track = track->Position();
									song.time = track->Length() / 1000;
									song.disc = album.disc;
									song.file = QString("%1.wav").arg(song.track);

									// Prefer title and artist from the track credits, but it appears to be empty if same as in Recording
									// Noticable in the musicbrainztest-fulldate test, where the title on the credits of track 18 are
									// "Bara om min älskade väntar", but the recording has title "Men bara om min älskade"
									if (recording && 0 == track->ArtistCredit()) {
										song.artist = artistFromCreditList(recording->ArtistCredit());
									}
									else {
										song.artist = artistFromCreditList(track->ArtistCredit());
									}

									if (recording && track->Title().empty()) {
										song.title = QString::fromUtf8(recording->Title().c_str());
									}
									else {
										song.title = QString::fromUtf8(track->Title().c_str());
									}
									album.tracks.append(song);
								}
							}

							// Ensure we have the same number of tracks as read from disc!
							if (album.tracks.count() < initial.tracks.count()) {
								for (int i = album.tracks.count(); i < initial.tracks.count(); ++i) {
									album.tracks.append(initial.tracks.at(i));
								}
							}
							m.append(album);
						}
					}
				}
			}
		}
	}
	catch (MusicBrainz5::CConnectionError& e) {
		DBUG << "MusicBrainz error" << e.what();
	}
	catch (MusicBrainz5::CTimeoutError& e) {
		DBUG << "MusicBrainz error - %1" << e.what();
	}
	catch (MusicBrainz5::CAuthenticationError& e) {
		DBUG << "MusicBrainz error - %1" << e.what();
	}
	catch (MusicBrainz5::CFetchError& e) {
		DBUG << "MusicBrainz error - %1" << e.what();
	}
	catch (MusicBrainz5::CRequestError& e) {
		DBUG << "MusicBrainz error - %1" << e.what();
	}
	catch (MusicBrainz5::CResourceNotFoundError& e) {
		DBUG << "MusicBrainz error - %1" << e.what();
	}

	if (m.isEmpty()) {
		if (!isInitial) {
			emit error(tr("No matches found in MusicBrainz"));
		}
	}
	else if (isInitial) {
		emit initialDetails(m.first());
	}
	else {
		emit matches(m);
	}
}

#include "moc_musicbrainz.cpp"
