/*
 * Cantata
 *
 * Copyright (c) 2011-2018 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 "network/networkproxyfactory.h"
#include <QNetworkProxy>
#include <QCryptographicHash>
#include <cstdio>
#include <cstring>
#include <musicbrainz5/Query.h>
#include <musicbrainz5/Medium.h>
#include <musicbrainz5/Release.h>
#include <musicbrainz5/ReleaseGroup.h>
#include <musicbrainz5/Track.h>
#include <musicbrainz5/Recording.h>
#include <musicbrainz5/Disc.h>
#include <musicbrainz5/HTTPFetch.h>
#include <musicbrainz5/ArtistCredit.h>
#include <musicbrainz5/Artist.h>
#include <musicbrainz5/NameCredit.h>
#include <QList>
#include <QRegExp>
#include "config.h"
#include "support/thread.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
#include <sys/cdio.h>
#include <arpa/inet.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);
    char temp[9];

    sprintf(temp, "%02X", 1);
    sha.addData(temp, strlen(temp));
    sprintf(temp, "%02X", numTracks);
    sha.addData(temp, strlen(temp));

    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;
        }

        sprintf(temp, "%08X", offset);
        sha.addData(temp, strlen(temp));
    }

    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());
                            QRegExp yearRe("^(\\d{4,4})(-\\d{1,2}-\\d{1,2})?$");
                            if (yearRe.indexIn(date) > -1) {
                                QString yearString = yearRe.cap(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"
