/***************************************************************************
    begin                : Sun May 15 2005
    copyright            : (C) 2005, 2008 by Michael Pyne
    email                : michael.pyne@kdemail.net
***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

#include "covermanager.h"

#include <QTimer>
#include <QPixmap>
#include <QString>
#include <QFile>
#include <QImage>
#include <QDir>
#include <QDataStream>
#include <QHash>
#include <QPixmapCache>
#include <QByteArray>
#include <QMap>

#include <kdebug.h>
#include <ktemporaryfile.h>
#include <kdemacros.h>
#include <kurl.h>
#include <kstandarddirs.h>
#include <kglobal.h>
#include <kio/job.h>

#include "juk.h"
#include "coverproxy.h"

// This is a dictionary to map the track path to their ID.  Otherwise we'd have
// to store this info with each CollectionListItem, which would break the cache
// of users who upgrade, and would just generally be a big mess.
typedef QHash<QString, coverKey> TrackLookupMap;

static const char dragMimetype[] = "application/x-juk-coverid";

const coverKey CoverManager::NoMatch = 0;

// Used to save and load CoverData from a QDataStream
QDataStream &operator<<(QDataStream &out, const CoverData &data);
QDataStream &operator>>(QDataStream &in, CoverData &data);

//
// Implementation of CoverSaveHelper class
//

CoverSaveHelper::CoverSaveHelper(QObject *parent) :
    QObject(parent),
    m_timer(new QTimer(this))
{
    connect(m_timer, SIGNAL(timeout()), SLOT(commitChanges()));

    // Wait 5 seconds before committing to avoid lots of disk activity for
    // rapid changes.

    m_timer->setSingleShot(true);
    m_timer->setInterval(5000);
}

void CoverSaveHelper::saveCovers()
{
    m_timer->start(); // Restarts if already triggered.
}

void CoverSaveHelper::commitChanges()
{
    CoverManager::saveCovers();
}

//
// Implementation of CoverData struct
//

QPixmap CoverData::pixmap() const
{
    return CoverManager::coverFromData(*this, CoverManager::FullSize);
}

QPixmap CoverData::thumbnail() const
{
    return CoverManager::coverFromData(*this, CoverManager::Thumbnail);
}

/**
 * This class is responsible for actually keeping track of the storage for the
 * different covers and such.  It holds the covers, and the map of path names
 * to cover ids, and has a few utility methods to load and save the data.
 *
 * @author Michael Pyne <michael.pyne@kdemail.net>
 * @see CoverManager
 */
class CoverManagerPrivate
{
public:

    /// Maps coverKey id's to CoverDataPtrs
    CoverDataMap covers;

    /// Maps file names to coverKey id's.
    TrackLookupMap tracks;

    /// A map of outstanding download KJobs to their coverKey
    QMap<KJob*, coverKey> downloadJobs;

    /// A static pixmap cache is maintained for covers, with key format of:
    /// 'f' followed by the pathname for FullSize covers, and
    /// 't' followed by the pathname for Thumbnail covers.
    /// However only thumbnails are currently cached.

    CoverManagerPrivate() : m_timer(new CoverSaveHelper(0)), m_coverProxy(0)
    {
        loadCovers();
    }

    ~CoverManagerPrivate()
    {
        delete m_timer;
        delete m_coverProxy;
        saveCovers();
    }

    void requestSave()
    {
        m_timer->saveCovers();
    }

    /**
     * Creates the data directory for the covers if it doesn't already exist.
     * Must be in this class for loadCovers() and saveCovers().
     */
    void createDataDir() const;

    /**
     * Returns the next available unused coverKey that can be used for
     * inserting new items.
     *
     * @return unused id that can be used for new CoverData
     */
    coverKey nextId() const;

    void saveCovers() const;

    CoverProxy *coverProxy() {
        if(!m_coverProxy)
            m_coverProxy = new CoverProxy;
        return m_coverProxy;
    }

    private:
    void loadCovers();

    /**
     * @return the full path and filename of the file storing the cover
     * lookup map and the translations between pathnames and ids.
     */
    QString coverLocation() const;

    CoverSaveHelper *m_timer;

    CoverProxy *m_coverProxy;
};

// This is responsible for making sure that the CoverManagerPrivate class
// gets properly destructed on shutdown.
K_GLOBAL_STATIC(CoverManagerPrivate, sd)

//
// Implementation of CoverManagerPrivate methods.
//
void CoverManagerPrivate::createDataDir() const
{
    QDir dir;
    QString dirPath(QDir::cleanPath(coverLocation() + "/.."));
    if(!dir.exists(dirPath))
        KStandardDirs::makeDir(dirPath);
}

void CoverManagerPrivate::saveCovers() const
{
    // Make sure the directory exists first.
    createDataDir();

    QFile file(coverLocation());

    kDebug() << "Opening covers db: " << coverLocation();

    if(!file.open(QIODevice::WriteOnly)) {
        kError() << "Unable to save covers to disk!\n";
        return;
    }

    QDataStream out(&file);

    // Write out the version and count
    out << quint32(0) << quint32(covers.count());

    kDebug() << "Writing out" << covers.count() << "covers.";

    // Write out the data
    for(CoverDataMap::const_iterator it = covers.begin(); it != covers.end(); ++it) {
        out << quint32(it.key());
        out << *it.value();
    }

    // Now write out the track mapping.
    out << quint32(tracks.count());

    kDebug() << "Writing out" << tracks.count() << "tracks.";

    TrackLookupMap::ConstIterator trackMapIt = tracks.constBegin();
    while(trackMapIt != tracks.constEnd()) {
        out << trackMapIt.key() << quint32(trackMapIt.value());
        ++trackMapIt;
    }
}

void CoverManagerPrivate::loadCovers()
{
    QFile file(coverLocation());

    if(!file.open(QIODevice::ReadOnly)) {
        // Guess we don't have any covers yet.
        return;
    }

    QDataStream in(&file);
    quint32 count, version;

    // First thing we'll read in will be the version.
    // Only version 0 is defined for now.
    in >> version;
    if(version > 0) {
        kError() << "Cover database was created by a higher version of JuK,\n";
        kError() << "I don't know what to do with it.\n";

        return;
    }

    // Read in the count next, then the data.
    in >> count;

    kDebug() << "Loading" << count << "covers.";
    for(quint32 i = 0; i < count; ++i) {
        // Read the id, and 3 QStrings for every 1 of the count.
        quint32 id;
        CoverDataPtr data(new CoverData);

        in >> id;
        in >> *data;
        data->refCount = 0;

        covers[(coverKey) id] = data;
    }

    in >> count;
    kDebug() << "Loading" << count << "tracks";
    for(quint32 i = 0; i < count; ++i) {
        QString path;
        quint32 id;

        in >> path >> id;

        // If we somehow already managed to load a cover id with this path,
        // don't do so again.  Possible due to a coding error during 3.5
        // development.

        if(KDE_ISLIKELY(!tracks.contains(path))) {
            ++covers[(coverKey) id]->refCount; // Another track using this.
            tracks.insert(path, id);
        }
    }

    kDebug() << "Tracks hash table has" << tracks.size() << "entries.";
}

QString CoverManagerPrivate::coverLocation() const
{
    return KGlobal::dirs()->saveLocation("appdata") + "coverdb/covers";
}

// XXX: This could probably use some improvement, I don't like the linear
// search for ID idea.  Linear search is used instead of covers.size() since we want to
// re-use old IDs if possible.
coverKey CoverManagerPrivate::nextId() const
{
    // Start from 1...
    coverKey key = 1;

    while(covers.contains(key))
        ++key;

    return key;
}

//
// Implementation of CoverDrag
//
CoverDrag::CoverDrag(coverKey id) :
    QMimeData()
{
    QPixmap cover = CoverManager::coverFromId(id);
    setImageData(cover.toImage());
    setData(dragMimetype, QByteArray::number(qulonglong(id), 10));
}

bool CoverDrag::isCover(const QMimeData *data)
{
    return data->hasImage() || data->hasFormat(dragMimetype);
}

coverKey CoverDrag::idFromData(const QMimeData *data)
{
    bool ok = false;

    if(!data->hasFormat(dragMimetype))
        return CoverManager::NoMatch;

    coverKey id = data->data(dragMimetype).toULong(&ok);
    if(!ok)
        return CoverManager::NoMatch;

    return id;
}

const char *CoverDrag::mimetype()
{
    return dragMimetype;
}

//
// Implementation of CoverManager methods.
//
coverKey CoverManager::idFromMetadata(const QString &artist, const QString &album)
{
    // Search for the string, yay!  It might make sense to use a cache here,
    // if so it's not hard to add a QCache.
    CoverDataMap::const_iterator it = begin();
    CoverDataMap::const_iterator endIt = end();

    for(; it != endIt; ++it) {
        if(it.value()->album == album.toLower() && it.value()->artist == artist.toLower())
            return it.key();
    }

    return NoMatch;
}

QPixmap CoverManager::coverFromId(coverKey id, Size size)
{
    CoverDataPtr info = coverInfo(id);

    if(!info)
        return QPixmap();

    if(size == Thumbnail)
        return info->thumbnail();

    return info->pixmap();
}

QPixmap CoverManager::coverFromData(const CoverData &coverData, Size size)
{
    QString path = coverData.path;

    // Prepend a tag to the path to separate in the cache between full size
    // and thumbnail pixmaps.  If we add a different kind of pixmap in the
    // future we also need to add a tag letter for it.
    if(size == FullSize)
        path.prepend('f');
    else
        path.prepend('t');

    // Check in cache for the pixmap.

    QPixmap pix;
    if(QPixmapCache::find(path, pix))
        return pix;

    // Not in cache, load it and add it.

    if(!pix.load(coverData.path))
        return QPixmap();

    // Only thumbnails are cached to avoid depleting global cache.  Caching
    // full size pics is not really useful as they are infrequently shown.

    if(size == Thumbnail) {
        // Double scale is faster and 99% as accurate
        QSize newSize(pix.size());
        newSize.scale(80, 80, Qt::KeepAspectRatio);
        pix = pix.scaled(2 * newSize)
                 .scaled(newSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
        QPixmapCache::insert(path, pix);
    }

    return pix;
}

coverKey CoverManager::addCover(const QPixmap &large, const QString &artist, const QString &album)
{
    kDebug() << "Adding new pixmap to cover database.\n";

    if(large.isNull()) {
        kDebug() << "The pixmap you're trying to add is NULL!\n";
        return NoMatch;
    }

    KTemporaryFile tempFile;
    if(!tempFile.open()) {
        kError() << "Unable to open file for pixmap cover, unable to add cover to DB\n";
        return NoMatch;
    }

    // Now that file is open, file name will be available, which is where we want
    // to save the pixmap as a .png.

    if(!large.save(tempFile.fileName(), "PNG")) {
        kError() << "Unable to save pixmap to " << tempFile.fileName() << endl;
        return NoMatch;
    }

    return addCover(KUrl::fromPath(tempFile.fileName()), artist, album);
}

coverKey CoverManager::addCover(const KUrl &path, const QString &artist, const QString &album)
{
    coverKey id = data()->nextId();
    CoverDataPtr coverData(new CoverData);

    QString fileNameExt = path.fileName();
    int extPos = fileNameExt.lastIndexOf('.');

    fileNameExt = fileNameExt.mid(extPos);
    if(extPos == -1)
        fileNameExt = "";

    // Copy it to a local file first.

    QString ext = QString("/coverdb/coverID-%1%2").arg(id).arg(fileNameExt);
    coverData->path = KGlobal::dirs()->saveLocation("appdata") + ext;

    kDebug() << "Saving pixmap to " << coverData->path;
    data()->createDataDir();

    coverData->artist = artist.toLower();
    coverData->album = album.toLower();
    coverData->refCount = 0;

    data()->covers[id] = coverData;

    // Can't use NetAccess::download() since if path is already a local file
    // (which is possible) then that function will return without copying, since
    // it assumes we merely want the file on the hard disk somewhere.

    KIO::FileCopyJob *job = KIO::file_copy(
         path, KUrl::fromPath(coverData->path),
         -1 /* perms */,KIO::HideProgressInfo | KIO::Overwrite
         );
    QObject::connect(job, SIGNAL(result(KJob*)),
                     data()->coverProxy(), SLOT(handleResult(KJob*)));
    data()->downloadJobs.insert(job, id);

    job->start();

    data()->requestSave(); // Save changes when possible.

    return id;
}

/**
 * This is called when our cover downloader has completed.  Typically there
 * should be no issues so we just need to ensure that the newly downloaded
 * cover is picked up by invalidating any cache entry for it.  If it didn't
 * download successfully we're in kind of a pickle as we've already assigned
 * a coverKey, which we need to go and erase.
 */
void CoverManager::jobComplete(KJob *job, bool completedSatisfactory)
{
    coverKey id = NoMatch;
    if(data()->downloadJobs.contains(job))
        id = data()->downloadJobs[job];

    if(id == NoMatch) {
        kError() << "No information on what download job" << job << "is.";
        data()->downloadJobs.remove(job);
        return;
    }

    if(!completedSatisfactory) {
        kError() << "Job" << job << "failed, but not handled yet.";
        removeCover(id);
        data()->downloadJobs.remove(job);
        JuK::JuKInstance()->coverDownloaded(QPixmap());
        return;
    }

    CoverDataPtr coverData = data()->covers[id];

    // Make sure the new cover isn't inadvertently cached.
    QPixmapCache::remove(QString("f%1").arg(coverData->path));
    QPixmapCache::remove(QString("t%1").arg(coverData->path));

    JuK::JuKInstance()->coverDownloaded(coverFromData(*coverData, CoverManager::Thumbnail));
}

bool CoverManager::hasCover(coverKey id)
{
    return data()->covers.contains(id);
}

bool CoverManager::removeCover(coverKey id)
{
    if(!hasCover(id))
        return false;

    // Remove cover from cache.
    CoverDataPtr coverData = coverInfo(id);
    QPixmapCache::remove(QString("f%1").arg(coverData->path));
    QPixmapCache::remove(QString("t%1").arg(coverData->path));

    // Remove references to files that had that track ID.
    QList<QString> affectedFiles = data()->tracks.keys(id);
    foreach (const QString &file, affectedFiles) {
        data()->tracks.remove(file);
    }

    // Remove covers from disk.
    QFile::remove(coverData->path);

    // Finally, forget that we ever knew about this cover.
    data()->covers.remove(id);
    data()->requestSave();

    return true;
}

bool CoverManager::replaceCover(coverKey id, const QPixmap &large)
{
    if(!hasCover(id))
        return false;

    CoverDataPtr coverData = coverInfo(id);

    // Empty old pixmaps from cache.
    QPixmapCache::remove(QString("t%1").arg(coverData->path));
    QPixmapCache::remove(QString("f%1").arg(coverData->path));

    large.save(coverData->path, "PNG");

    // No save is needed, as all that has changed is the on-disk cover data,
    // not the list of tracks or covers.

    return true;
}

CoverManagerPrivate *CoverManager::data()
{
    return sd;
}

void CoverManager::saveCovers()
{
    data()->saveCovers();
}

void CoverManager::shutdown()
{
    sd.destroy();
}

CoverDataMapIterator CoverManager::begin()
{
    return data()->covers.constBegin();
}

CoverDataMapIterator CoverManager::end()
{
    return data()->covers.constEnd();
}

CoverList CoverManager::keys()
{
    return data()->covers.keys();
}

void CoverManager::setIdForTrack(const QString &path, coverKey id)
{
    coverKey oldId = data()->tracks.value(path, NoMatch);
    if(data()->tracks.contains(path) && (id == oldId))
        return; // We're already done.

    if(oldId != NoMatch) {
        data()->covers[oldId]->refCount--;
        data()->tracks.remove(path);

        if(data()->covers[oldId]->refCount == 0) {
            kDebug() << "Cover " << oldId << " is unused, removing.\n";
            removeCover(oldId);
        }
    }

    if(id != NoMatch) {
        data()->covers[id]->refCount++;
        data()->tracks.insert(path, id);
    }

    data()->requestSave();
}

coverKey CoverManager::idForTrack(const QString &path)
{
    return data()->tracks.value(path, NoMatch);
}

CoverDataPtr CoverManager::coverInfo(coverKey id)
{
    if(data()->covers.contains(id))
        return data()->covers[id];

    return CoverDataPtr(0);
}

/**
 * Write @p data out to @p out.
 *
 * @param out the data stream to write @p data out to.
 * @param data the CoverData to write out.
 * @return the data stream that the data was written to.
 */
QDataStream &operator<<(QDataStream &out, const CoverData &data)
{
    out << data.artist;
    out << data.album;
    out << data.path;

    return out;
}

/**
 * Read @p data from @p in.
 *
 * @param in the data stream to read from.
 * @param data the CoverData to read into.
 * @return the data stream read from.
 */
QDataStream &operator>>(QDataStream &in, CoverData &data)
{
    in >> data.artist;
    in >> data.album;
    in >> data.path;

    return in;
}

// vim: set et sw=4 tw=0 sta:
