/*
 * Cantata
 *
 * Copyright (c) 2011-2014 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 "mtpdevice.h"
#include "models/musiclibrarymodel.h"
#include "models/musiclibraryitemsong.h"
#include "models/musiclibraryitemalbum.h"
#include "models/musiclibraryitemartist.h"
#include "models/musiclibraryitemroot.h"
#include "models/dirviewmodel.h"
#include "devicepropertiesdialog.h"
#include "devicepropertieswidget.h"
#include "gui/covers.h"
#include "mpd/song.h"
#include "encoders.h"
#include "transcodingjob.h"
#include "support/utils.h"
#include "mpd/mpdparseutils.h"
#include "mpd/mpdconnection.h"
#include "support/localize.h"
#include "filejob.h"
#include "support/configuration.h"
#include "support/thread.h"
#include <QTimer>
#include <QDir>
#include <QTemporaryFile>
#ifdef ENABLE_KDE_SUPPORT
#include <KDE/KMimeType>
#include <solid/genericinterface.h>
#else
#include "solid-lite/genericinterface.h"
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
//#define TIME_MTP_OPERATIONS
#ifdef TIME_MTP_OPERATIONS
#include <QDebug>
#include <QElapsedTimer>
#endif

// Enable the following #define to have Cantata attempt to ascertain the AlbumArtist tag by
// looking at the file path
#define MTP_FAKE_ALBUMARTIST_SUPPORT

// Enable the following #define to have Cantata attempt to ascertain the tracks's track number
// from its filename. This will only be done if the device returns '0' as the track number.
#define MTP_TRACKNUMBER_FROM_FILENAME

static const QLatin1String constMtpDefaultCover("AlbumArt.jpg");
static const uint32_t constRootFolder=0xffffffffU;
static const QString constMusicFolder=QLatin1String("Music");

static int progressMonitor(uint64_t const processed, uint64_t const total, void const * const data)
{
    const MtpConnection *con=static_cast<const MtpConnection *>(data);
    const_cast<MtpConnection *>(con)->emitProgress((int)(((processed*1.0)/(total*1.0)*100.0)+0.5));
    return con->abortWasRequested() ? -1 : 0;
}

static int trackListMonitor(uint64_t const processed, uint64_t const total, void const * const data)
{
    const MtpConnection *con=static_cast<const MtpConnection *>(data);
    const_cast<MtpConnection *>(con)->trackListProgress(((processed*100.0)/(total*1.0))+0.5);
    return con->abortWasRequested() ? -1 : 0;
}

static uint16_t fileReceiver(void *params, void *priv, uint32_t sendlen, unsigned char *data, uint32_t *putlen)
{
    Q_UNUSED(params)
    QByteArray *byteData=static_cast<QByteArray *>(priv);
    (*byteData)+=QByteArray((char *)data, (int)sendlen);
    *putlen = sendlen;
    return LIBMTP_HANDLER_RETURN_OK;
}

MtpConnection::MtpConnection(unsigned int bus, unsigned int dev, bool aaSupport)
    : device(0)
    #ifdef MTP_CLEAN_ALBUMS
    , albums(0)
    #endif
    , library(0)
    , lastListPercent(-1)
    , abortRequested(false)
    , busNum(bus)
    , devNum(dev)
    , supprtAlbumArtistTag(aaSupport)
{
    size=0;
    used=0;
    LIBMTP_Init();
    thread=new Thread(metaObject()->className());
    moveToThread(thread);
    thread->start();
}

MtpConnection::~MtpConnection()
{
    stop();
}

MusicLibraryItemRoot * MtpConnection::takeLibrary()
{
    MusicLibraryItemRoot *lib=library;
    library=0;
    return lib;
}

void MtpConnection::emitProgress(int percent)
{
    emit progress(percent);
}

void MtpConnection::trackListProgress(int percent)
{
    if (percent!=lastListPercent) {
        lastListPercent=percent;
        emit updatePercentage(percent);
    }
}

void MtpConnection::connectToDevice()
{
    #ifdef TIME_MTP_OPERATIONS
    QElapsedTimer timer;
    QElapsedTimer totalTimer;
    timer.start();
    totalTimer.start();
    #endif

    device=0;
    storage.clear();
    defaultMusicFolder=0;
    LIBMTP_raw_device_t *rawDevices=0;
    int numDev=-1;
    emit statusMessage(i18n("Connecting to device..."));
    if (LIBMTP_ERROR_NONE!=LIBMTP_Detect_Raw_Devices(&rawDevices, &numDev) || numDev<=0) {
        emit statusMessage(i18n("No devices found"));
        return;
    }
    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "Connect to device:" << timer.elapsed();
    timer.restart();
    #endif

    LIBMTP_mtpdevice_t *mptDev=0;
    for (int i = 0; i < numDev; i++) {
        if (0!=busNum && 0!=devNum) {
            if (rawDevices[i].bus_location==busNum && rawDevices[i].devnum==devNum) {
                #ifdef ENABLE_UNCACHED_MTP
                mptDev = LIBMTP_Open_Raw_Device_Uncached(&rawDevices[i]);
                #else
                mptDev = LIBMTP_Open_Raw_Device(&rawDevices[i]);
                #endif
                break;
            }
        } else {
            #ifdef ENABLE_UNCACHED_MTP
            if ((mptDev = LIBMTP_Open_Raw_Device_Uncached(&rawDevices[i]))) {
                break;
            }
            #else
            if ((mptDev = LIBMTP_Open_Raw_Device(&rawDevices[i]))) {
                break;
            }
            #endif
        }
    }

    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "Open raw device:" << timer.elapsed();
    timer.restart();
    #endif

    size=0;
    used=0;
    device=mptDev;
    updateStorage();
    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "Update storage:" << timer.elapsed();
    #endif

    free(rawDevices);
    if (!device) {
        emit statusMessage(i18n("No devices found"));
        #ifdef TIME_MTP_OPERATIONS
        qWarning() << "TOTAL connect:" <<totalTimer.elapsed();
        #endif
        return;
    }
    char *ser=LIBMTP_Get_Serialnumber(device);
    if (ser) {
        emit deviceDetails(QString::fromUtf8(ser));
        delete ser;
    } else {
        emit deviceDetails(QString());
    }

    defaultMusicFolder=device->default_music_folder;
    emit statusMessage(i18n("Connected to device"));
    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "TOTAL connect:" <<totalTimer.elapsed();
    #endif
}

void MtpConnection::disconnectFromDevice(bool showStatus)
{
    if (device) {
        destroyData();
        LIBMTP_Release_Device(device);
        device=0;
        if (showStatus) {
            emit statusMessage(i18n("Disconnected from device"));
        }
    }
}

#ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
struct MtpAlbum {
    uint32_t folder;
    QSet<QString> artists;
    QList<MusicLibraryItemSong *> songs;
};
struct MtpFolder {
    MtpFolder(const QString &ar=QString(), const QString &al=QString()) : artist(ar), album(al) { }
    QString artist;
    QString album;
};
#endif

struct Path {
    Path() : storage(0), parent(0), id(0) { }
    bool ok() const { return 0!=storage && 0!=parent && 0!=id; }
    uint32_t storage;
    uint32_t parent;
    uint32_t id;
    QString path;
};

static QString encodePath(LIBMTP_track_t *track, const QString &path, const QString &store)
{
    return QChar('{')+QString::number(track->storage_id)+QChar('/')+QString::number(track->parent_id)+QChar('/')+QString::number(track->item_id)+
           (store.isEmpty() ? QString() : (QChar('/')+store))+QChar('}')+path;
}

static Path decodePath(const QString &path)
{
    Path p;
    if (path.startsWith(QChar('{')) && path.contains(QChar('}'))) {
        int end=path.indexOf(QChar('}'));
        QStringList details=path.mid(1, end-1).split(QChar('/'));
        if (details.count()>=3) {
            p.storage=details.at(0).toUInt();
            p.parent=details.at(1).toUInt();
            p.id=details.at(2).toUInt();
        }
        p.path=path.mid(end+1);
    } else {
        p.path=path;
    }

    return p;
}

void MtpConnection::updateLibrary(const DeviceOptions &opts)
{
    if (!isConnected()) {
        connectToDevice();
    }

    destroyData();

    if (!isConnected()) {
        emit libraryUpdated();
        return;
    }

    #ifdef TIME_MTP_OPERATIONS
    QElapsedTimer timer;
    QElapsedTimer totalTimer;
    timer.start();
    totalTimer.start();
    #endif

    library = new MusicLibraryItemRoot;
    emit statusMessage(i18n("Updating folders..."));
    #ifdef ENABLE_UNCACHED_MTP
    updateFilesAndFolders();
    if (abortRequested) {
        return;
    }
    #else
    updateFolders();
    #endif
    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "Folder update:" << timer.elapsed();
    timer.restart();
    #endif
    if (folderMap.isEmpty()) {
        destroyData();
        emit libraryUpdated();
        return;
    }
    #ifndef ENABLE_UNCACHED_MTP
    emit statusMessage(i18n("Updating files..."));
    updateFiles();
    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "Files update:" << timer.elapsed();
    timer.restart();
    #endif
    #endif
    #ifdef MTP_CLEAN_ALBUMS
    updateAlbums();
    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "Clean albums:" << timer.elapsed();
    timer.restart();
    #endif
    #endif
    emit statusMessage(i18n("Updating tracks..."));
    lastListPercent=-1;
    LIBMTP_track_t *tracks=LIBMTP_Get_Tracklisting_With_Callback(device, &trackListMonitor, this);
    QMap<uint32_t, Folder>::ConstIterator folderEnd=folderMap.constEnd();
    QList<Storage>::Iterator store=storage.begin();
    QList<Storage>::Iterator storeEnd=storage.end();
    QMap<int, QString> storageNames;
    for (; store!=storeEnd; ++store) {
        setMusicFolder(*store);
        storageNames[(*store).id]=(*store).description;
    }
    if (abortRequested) {
        return;
    }

    MusicLibraryItemArtist *artistItem = 0;
    MusicLibraryItemAlbum *albumItem = 0;
    #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
    QMap<QString, MtpAlbum> albumMap;
    QMap<uint32_t, MtpFolder> folders;
    bool getAlbumArtistFromPath=opts.scheme.startsWith(DeviceOptions::constAlbumArtist+QChar('/')+DeviceOptions::constAlbumTitle+QChar('/'));
    #endif
    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "Tracks update:" << timer.elapsed();
    timer.restart();
    #endif
    while (tracks) {
        if (abortRequested) {
            return;
        }
        LIBMTP_track_t *track=tracks;
        tracks=tracks->next;

        QMap<uint32_t, Folder>::ConstIterator folder=folderMap.find(track->parent_id);
        if (folder==folderEnd) {
            // We only care about tracks in the music folder...
            LIBMTP_destroy_track_t(track);
            continue;
        }
        Song s;
        QString trackFilename=QString::fromUtf8(track->filename);
        s.id=track->item_id;
        s.file=encodePath(track, folder.value().path+trackFilename, storageNames.count()>1 ? storageNames[track->storage_id] : QString());
        s.album=QString::fromUtf8(track->album);
        s.artist=QString::fromUtf8(track->artist);
        s.albumartist=s.artist; // TODO: ALBUMARTIST: Read from 'track' when libMTP supports album artist!
        QString composer=QString::fromUtf8(track->composer);
        if (!composer.isEmpty()) {
            s.setComposer(composer);
        }
        s.year=QString::fromUtf8(track->date).mid(0, 4).toUInt();
        s.title=QString::fromUtf8(track->title);
        s.genre=QString::fromUtf8(track->genre);
        s.track=track->tracknumber;
        s.time=(track->duration/1000.0)+0.5;
        s.size=track->filesize;
        s.fillEmptyFields();
        #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
        if (getAlbumArtistFromPath) {
            QStringList folderParts=(*folder).path.split('/', QString::SkipEmptyParts);
            if (folderParts.length()>=3) {
                // Path should be "Music/${AlbumArtist}/${Album}"
                int artistPath=1;
                int albumPath=2;
                // BubbleUPNP will download to "Music/Artist/${AlbumArtist}/${Album}" if it is configured to
                // download to "Music" and "Preserve folder structure" (At least this is the folder structure
                // from MiniDLNA...)
                if (folderParts.length()>=4 && QLatin1String("Artist")==folderParts.at(1)) {
                    artistPath++;
                    albumPath++;
                }
                MtpFolder folder(folderParts.at(artistPath), folderParts.at(albumPath));
                folders.insert(track->parent_id, folder);
                if (folder.album==s.album && Song::isVariousArtists(folder.artist)) {
                    s.albumartist=folder.artist;
                }
            }
        }
        #endif
        #ifdef MTP_TRACKNUMBER_FROM_FILENAME
        if (0==s.track) {
            int space=trackFilename.indexOf(' ');
            if (space>0 && space<=3) {
                s.track=trackFilename.mid(0, space).toInt();
            }
        }
        #endif
        if (!artistItem || (supprtAlbumArtistTag ? s.artistOrComposer()!=artistItem->data() : s.artist!=artistItem->data())) {
            artistItem = library->artist(s);
        }
        if (!albumItem || albumItem->parentItem()!=artistItem || s.albumName()!=albumItem->data()) {
            albumItem = artistItem->album(s);
        }
        MusicLibraryItemSong *songItem = new MusicLibraryItemSong(s, albumItem);
        const QSet<QString> &songGenres=songItem->allGenres();
        albumItem->append(songItem);
        albumItem->addGenres(songGenres);
        artistItem->addGenres(songGenres);
        library->addGenres(songGenres);

        #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
        // Store AlbumName->Artists/Songs mapping
        MtpAlbum &al=albumMap[s.album];
        al.artists.insert(s.artist);
        al.songs.append(songItem);
        al.folder=track->parent_id;
        #endif
        LIBMTP_destroy_track_t(track);
    }

    while (tracks) {
        LIBMTP_track_t *track=tracks;
        tracks=tracks->next;
        LIBMTP_destroy_track_t(track);
    }

    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "Tracks parse:" << timer.elapsed();
    timer.restart();
    #endif
    #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
    // Use Album map to determine 'AlbumArtist' tag for various artist albums, and
    // albums that have tracks where artist is set to '${artist} and somebodyelse'
    QMap<QString, MtpAlbum>::ConstIterator it=albumMap.constBegin();
    QMap<QString, MtpAlbum>::ConstIterator end=albumMap.constEnd();
    for (; it!=end; ++it) {
        if (abortRequested) {
            return;
        }
        if ((*it).artists.count()>1) {
            QSet<quint16> tracks;
            QString shortestArtist;
            bool duplicateTrackNumbers=false;
            foreach (MusicLibraryItemSong *s, (*it).songs) {
                if (tracks.contains(s->track())) {
                    duplicateTrackNumbers=true;
                    break;
                } else {
                    if (shortestArtist.isEmpty() || s->song().artist.length()<shortestArtist.length()) {
                        shortestArtist=s->song().artist;
                    }
                    tracks.insert(s->track());
                }
            }
            // If an album has mutiple tracks with the same track number, then we probably have X albums
            // by X artists - in which case we proceeed no further.
            if (!duplicateTrackNumbers) {
                MtpFolder &f=folders[(*it).folder];
                // Now, check to see if all artists contain 'shortestArtist'. If so then use 'shortestArtist' as the album
                // artist. This is probably due to songs which have artist set to '${artist} and somebodyelse'
                QString albumArtist=shortestArtist;
                foreach (const QString &artist, (*it).artists) {
                    if (!artist.contains(shortestArtist)) {
                        // Found an artist that did not contain 'shortestArtist', so use 'Various Artists' for album artist
                        albumArtist=!f.artist.isEmpty() && f.album==it.key() ? f.artist : Song::variousArtists();
                        break;
                    }
                }

                // Now move songs to correct artist/album...
                foreach (MusicLibraryItemSong *s, (*it).songs) {
                    if (s->song().albumartist==albumArtist) {
                        continue;
                    }
                    Song song=s->song();
                    song.albumartist=albumArtist;
                    artistItem=library->artist(song);
                    albumItem=artistItem->album(song);
                    MusicLibraryItemSong *songItem = new MusicLibraryItemSong(song, albumItem);
                    albumItem->append(songItem);
                    albumItem->updateGenres();
                    artistItem->updateGenres();
                    MusicLibraryItemAlbum *prevAlbum=(MusicLibraryItemAlbum *)s->parentItem();
                    prevAlbum->remove(s);
                    if (0==prevAlbum->childCount()) {
                        // Album no longer has any songs, so remove!
                        MusicLibraryItemArtist *prevArtist=(MusicLibraryItemArtist *)prevAlbum->parentItem();
                        prevArtist->remove(prevAlbum);
                        if (0==prevArtist->childCount()) {
                            // Artist no longer has any albums, so remove!
                            library->remove(prevArtist);
                        }
                    }
                }
            }
        }
    }
    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "AlbumArtist detection:" << timer.elapsed();
    timer.restart();
    #endif
    #endif
    if (abortRequested) {
        return;
    }
    library->applyGrouping();
    #ifdef TIME_MTP_OPERATIONS
    qWarning() << "Grouping:" << timer.elapsed();
    qWarning() << "TOTAL update:" <<totalTimer.elapsed();
    #endif
    emit libraryUpdated();
}

void MtpConnection::setMusicFolder(Storage &store)
{
    if (0==store.musicFolderId) {
        store.musicFolderId=getFolder(constMusicFolder+QLatin1Char('/'), store.id);
        if (0==store.musicFolderId) {
            store.musicFolderId=createFolder(constMusicFolder, constMusicFolder+QLatin1Char('/'), 0, store.id);
        }
        if (0!=store.musicFolderId) {
            store.musicPath=getPath(store.musicFolderId);
        }
    }
}

#ifdef ENABLE_UNCACHED_MTP
void MtpConnection::updateFilesAndFolders()
{
    folderMap.clear();
    foreach (const Storage st, storage) {
        if (abortRequested) {
            return;
        }
        listFolder(st.id, constRootFolder, 0);
    }
}

void MtpConnection::listFolder(uint32_t storage, uint32_t parentDir, Folder *f)
{
    LIBMTP_file_t *files=LIBMTP_Get_Files_And_Folders(device, storage, parentDir);

    while (files && !abortRequested) {
        LIBMTP_file_t *file=files;
        files = files->next;

        QString name=QString::fromUtf8(file->filename);
        if (LIBMTP_FILETYPE_FOLDER==file->filetype) {
            bool isMusic=constRootFolder!=parentDir || 0==name.compare(constMusicFolder, Qt::CaseInsensitive);
            if (isMusic) {
                QMap<uint32_t, Folder>::ConstIterator it=folderMap.find(file->parent_id);
                QString path;
                if (it!=folderMap.constEnd()) {
                    path=it.value().path+name+QLatin1Char('/');
                } else {
                    path=name+QLatin1Char('/');
                }

                QMap<uint32_t, Folder>::iterator entry=folderMap.insert(file->item_id, Folder(path, file->item_id, file->parent_id, file->storage_id));
                if (folderMap.contains(file->parent_id)) {
                    folderMap[file->parent_id].folders.insert(file->item_id);
                }
                listFolder(storage, file->item_id, &(entry.value()));
            }
        } else if (f && constRootFolder!=parentDir) {
            if (name.endsWith(".jpg", Qt::CaseInsensitive) || name.endsWith(".png", Qt::CaseInsensitive) || QLatin1String("albumart.pamp")==name) {
                f->covers.insert(file->item_id, File(name, file->filesize, file->item_id));
            } else {
                f->files.insert(file->item_id, File(name, file->filesize, file->item_id));
            }
        }
        LIBMTP_destroy_file_t(file);
    }
}
#else
void MtpConnection::updateFolders()
{
    folderMap.clear();
    LIBMTP_folder_t *folders=LIBMTP_Get_Folder_List(device);
    parseFolder(folders);
    if (folders) {
        // LIBMTP_destroy_folder_t is recursive
        LIBMTP_destroy_folder_t(folders);
    }
}

void MtpConnection::updateFiles()
{
    LIBMTP_file_t *files=LIBMTP_Get_Filelisting_With_Callback(device, 0, 0);
    QSet<uint32_t> folders=folderMap.keys().toSet();
    while (files) {
        LIBMTP_file_t *file=files;
        files=files->next;

        if (folders.contains(file->parent_id)) {
            Folder &folder=folderMap[file->parent_id];
            if (LIBMTP_FILETYPE_FOLDER==file->filetype) {
                folder.folders.insert(file->item_id);
            } else {
                QString name=QString::fromUtf8(file->filename);
                if (name.endsWith(".jpg", Qt::CaseInsensitive) || name.endsWith(".png", Qt::CaseInsensitive) || QLatin1String("albumart.pamp")==name) {
                    folder.covers.insert(file->item_id, File(name, file->filesize, file->item_id));
                } else {
                    folder.files.insert(file->item_id, File(name, file->filesize, file->item_id));
                }
            }
        }
        LIBMTP_destroy_file_t(file);
    }
}
#endif

void MtpConnection::updateStorage()
{
    uint64_t sizeCalc=0;
    uint64_t usedCalc=0;
    if (device && LIBMTP_ERROR_NONE==LIBMTP_Get_Storage(device, LIBMTP_STORAGE_SORTBY_MAXSPACE)) {
        LIBMTP_devicestorage_struct *s=device->storage;
        while (s) {
            QString volumeIdentifier=QString::fromUtf8(s->VolumeIdentifier);
            QList<Storage>::Iterator it=storage.begin();
            QList<Storage>::Iterator end=storage.end();
            for ( ;it!=end; ++it) {
                if ((*it).volumeIdentifier==volumeIdentifier) {
                    // We know about this storage ID, so update its size...
                    (*it).size=s->MaxCapacity;
                    (*it).used=s->MaxCapacity-s->FreeSpaceInBytes;
                    break;
                }
            }

            if (it==end) {
                // Unknown storage ID, so add to list!
                Storage store;
                store.id=s->id;
                store.description=QString::fromUtf8(s->StorageDescription);
                store.volumeIdentifier=QString::fromUtf8(s->VolumeIdentifier);
                store.size=s->MaxCapacity;
                store.used=s->MaxCapacity-s->FreeSpaceInBytes;
                storage.append(store);
            }
            sizeCalc+=s->MaxCapacity;
            usedCalc+=s->MaxCapacity-s->FreeSpaceInBytes;
            s=s->next;
        }
    }
    size=sizeCalc;
    used=usedCalc;
}

QList<DeviceStorage> MtpConnection::getStorageList() const
{
    QList<DeviceStorage> s;
    QList<Storage>::ConstIterator it=storage.constBegin();
    QList<Storage>::ConstIterator end=storage.constEnd();
    for ( ;it!=end; ++it) {
        DeviceStorage store;
        store.size=(*it).size;
        store.used=(*it).used;
        store.description=(*it).description;
        store.volumeIdentifier=(*it).volumeIdentifier;
        s.append(store);
    }

    return s;
}

MtpConnection::Storage & MtpConnection::getStorage(const QString &volumeIdentifier)
{
    QList<Storage>::Iterator first=storage.begin();
    if (!volumeIdentifier.isEmpty()) {
        QList<Storage>::Iterator it=first;
        QList<Storage>::Iterator end=storage.end();
        for ( ;it!=end; ++it) {
            if ((*it).volumeIdentifier==volumeIdentifier) {
                return *it;
            }
        }
    }

    return *first;
}

MtpConnection::Storage & MtpConnection::getStorage(uint32_t id)
{
    QList<Storage>::Iterator first=storage.begin();
    QList<Storage>::Iterator it=first;
    QList<Storage>::Iterator end=storage.end();
    for ( ;it!=end; ++it) {
        if ((*it).id==id) {
            return *it;
        }
    }

    return *first;
}

uint32_t MtpConnection::createFolder(const QString &name, const QString &fullPath, uint32_t parentId, uint32_t storageId)
{
    char *nameCopy = qstrdup(name.toUtf8().constData());
    uint32_t newFolderId=LIBMTP_Create_Folder(device, nameCopy, parentId, storageId);
    delete nameCopy;
    if (0==newFolderId)  {
        return 0;
    }

    folderMap.insert(newFolderId, Folder(fullPath, newFolderId, parentId, storageId));
    return newFolderId;
}

uint32_t MtpConnection::getFolder(const QString &path, uint32_t storageId)
{
    QMap<uint32_t, Folder>::ConstIterator it=folderMap.constBegin();
    QMap<uint32_t, Folder>::ConstIterator end=folderMap.constEnd();

    for (; it!=end; ++it) {
        if (storageId==(*it).storageId && 0==(*it).path.compare(path, Qt::CaseInsensitive)) {
            return (*it).id;
        }
    }
    return 0;
}

QString MtpConnection::getPath(uint32_t folderId)
{
    return folderMap.contains(folderId) ? folderMap[folderId].path : QString();
}

uint32_t MtpConnection::checkFolderStructure(const QStringList &dirs, Storage &store)
{
    setMusicFolder(store);
    QString path;
    uint32_t parentId=store.musicFolderId;

    foreach (const QString &d, dirs) {
        path+=d+QChar('/');
        uint32_t folderId=getFolder(path, store.id);
        if (0==folderId) {
            folderId=createFolder(d, path, parentId, store.id);
            if (0==folderId) {
                return parentId;
            } else {
                parentId=folderId;
            }
        } else {
            parentId=folderId;
        }
    }
    return parentId;
}

#ifndef ENABLE_UNCACHED_MTP
void MtpConnection::parseFolder(LIBMTP_folder_t *folder)
{
    if (!folder) {
        return;
    }

    QMap<uint32_t, Folder>::ConstIterator it=folderMap.find(folder->parent_id);
    QString path;
    if (it!=folderMap.constEnd()) {
        path=it.value().path+QString::fromUtf8(folder->name)+QChar('/');
    } else {
        path=QString::fromUtf8(folder->name)+QChar('/');
    }
    bool isMusic=path.startsWith(constMusicFolder+QLatin1Char('/'), Qt::CaseInsensitive);
    if (isMusic) {
        folderMap.insert(folder->folder_id, Folder(path, folder->folder_id, folder->parent_id, folder->storage_id));
        if (folderMap.contains(folder->parent_id)) {
            folderMap[folder->parent_id].folders.insert(folder->folder_id);
        }
    }
    // Only recurse into music folder...
    if (folder->child && isMusic) {
        parseFolder(folder->child);
    }
    if (folder->sibling) {
        parseFolder(folder->sibling);
    }
}
#endif

static char * createString(const QString &str)
{
    return str.isEmpty() ? qstrdup("") : qstrdup(str.toUtf8());
}

static LIBMTP_filetype_t mtpFileType(const QString &f)
{
    #ifdef ENABLE_KDE_SUPPORT
    KMimeType::Ptr mime=KMimeType::findByPath(f);

    if (mime->is("audio/mpeg")) {
        return LIBMTP_FILETYPE_MP3;
    }
    if (mime->is("audio/ogg")) {
        return LIBMTP_FILETYPE_OGG;
    }
    if (mime->is("audio/x-ms-wma")) {
        return LIBMTP_FILETYPE_WMA;
    }
    if (mime->is("audio/mp4")) {
        return LIBMTP_FILETYPE_M4A; // LIBMTP_FILETYPE_MP4
    }
    if (mime->is("audio/aac")) {
        return LIBMTP_FILETYPE_AAC;
    }
    if (mime->is("audio/flac")) {
        return LIBMTP_FILETYPE_FLAC;
    }
    if (mime->is("audio/x-wav")) {
        return LIBMTP_FILETYPE_WAV;
    }
    if (mime->is("image/jpeg")) {
        return LIBMTP_FILETYPE_JPEG;
    }
    if (mime->is("image/png")) {
        return LIBMTP_FILETYPE_PNG;
    }
    #else
    if (f.endsWith(".mp3", Qt::CaseInsensitive)) {
        return LIBMTP_FILETYPE_MP3;
    }
    if (f.endsWith(".ogg", Qt::CaseInsensitive)) {
        return LIBMTP_FILETYPE_OGG;
    }
    if (f.endsWith(".wma", Qt::CaseInsensitive)) {
        return LIBMTP_FILETYPE_WMA;
    }
    if (f.endsWith(".m4a", Qt::CaseInsensitive)) {
        return LIBMTP_FILETYPE_M4A; // LIBMTP_FILETYPE_MP4
    }
    if (f.endsWith(".aac", Qt::CaseInsensitive)) {
        return LIBMTP_FILETYPE_AAC;
    }
    if (f.endsWith(".flac", Qt::CaseInsensitive)) {
        return LIBMTP_FILETYPE_FLAC;
    }
    if (f.endsWith(".wav", Qt::CaseInsensitive)) {
        return LIBMTP_FILETYPE_WAV;
    }
    if (f.endsWith(".jpg", Qt::CaseInsensitive)) {
        return LIBMTP_FILETYPE_JPEG;
    }
    if (f.endsWith(".png", Qt::CaseInsensitive)) {
        return LIBMTP_FILETYPE_PNG;
    }
    #endif
    return LIBMTP_FILETYPE_UNDEF_AUDIO;
}

static QTemporaryFile * saveImageToTemp(const QImage &img, const QString &name)
{
    QTemporaryFile *temp=new QTemporaryFile();

    int index=name.lastIndexOf('.');
    if (index>0) {
        temp=new QTemporaryFile(QDir::tempPath()+"/cantata_XXXXXX"+name.mid(index));
    } else {
        temp=new QTemporaryFile(QDir::tempPath()+"/cantata_XXXXXX");
    }
    img.save(temp);
    temp->close();
    return temp;
}

void MtpConnection::putSong(const Song &s, bool fixVa, const DeviceOptions &opts, bool overwrite, bool copyCover)
{
    int status=Device::Failed;
    bool fixedVa=false;
    bool embedCoverImage=Device::constEmbedCover==opts.coverName;
    bool copiedCover=false;
    LIBMTP_track_t *meta=0;
    QString destName;
    Storage store=getStorage(opts.volumeId);
    uint32_t folderId=0;

    copyCover=copyCover && !embedCoverImage && Device::constNoCover!=opts.coverName;
    if (device) {
        meta=LIBMTP_new_track_t();
        meta->item_id=0;
        Song song=s;
        QString fileName=song.file;
        QTemporaryFile *temp=0;

        if (fixVa || embedCoverImage) {
            // Need to 'workaround' broken various artists handling, and/or embedding cover, so write to a temporary file first...
            temp=Device::copySongToTemp(song);
            if (temp) {
                if (embedCoverImage) {
                    Device::embedCover(song.file, song, opts.coverMaxSize);
                }
                if (fixVa && Device::fixVariousArtists(temp->fileName(), song, true)) {
                    fixedVa=true;
                }
                song.file=temp->fileName();
            }
        }
        struct stat statBuf;
        if (0==stat(QFile::encodeName(song.file).constData(), &statBuf)) {
            meta->filesize=statBuf.st_size;
            meta->modificationdate=statBuf.st_mtime;
        }

        // Check if storage has enough space, if not try to find one that does!
        if (store.freeSpace()<meta->filesize) {
            QList<Storage>::Iterator it=storage.begin();
            QList<Storage>::Iterator end=storage.end();
            for ( ;it!=end; ++it) {
                if ((*it).freeSpace()>=meta->filesize) {
                    store=*it;
                    break;
                }
            }
        }

        meta->parent_id=folderId=store.musicFolderId;
        meta->storage_id=store.id;
        destName=store.musicPath+opts.createFilename(song);
        QStringList dirs=destName.split('/', QString::SkipEmptyParts);
        if (dirs.count()>1) {
            destName=dirs.takeLast();
            meta->parent_id=folderId=checkFolderStructure(dirs, store);
        }
        Folder &folder=folderMap[folderId];
        QMap<uint32_t, File>::ConstIterator it=folder.files.constBegin();
        QMap<uint32_t, File>::ConstIterator end=folder.files.constEnd();
        for (; it!=end; ++it) {
            if ((*it).name==destName) {
                if (!overwrite || 0!=LIBMTP_Delete_Object(device, (*it).id)) {
                    status=Device::SongExists;
                }
                break;
            }
        }

        if (status!=Device::SongExists) {
            meta->title=createString(song.title);
            meta->artist=createString(song.artist);
            meta->composer=createString(song.composer());
            meta->genre=createString(song.genre);
            meta->album=createString(song.album);
            meta->date=createString(QString().sprintf("%4d0101T0000.0", song.year));
            meta->filename=createString(destName);
            meta->tracknumber=song.track;
            meta->duration=song.time*1000;
            meta->rating=0;
            meta->usecount=0;
            meta->filetype=mtpFileType(song.file);
            meta->next=0;
            switch (LIBMTP_Send_Track_From_File(device, fileName.toUtf8(), meta, &progressMonitor, this)) {
            case LIBMTP_ERROR_NONE:         status=Device::Ok;      break;
            case LIBMTP_ERROR_STORAGE_FULL: status=Device::NoSpace; break;
            default:                        status=Device::Failed;  break;
            }
        }
        if (temp) {
            // Delete the temp file...
            temp->remove();
            delete temp;
        }
    }

    if (Device::Ok==status) {
        Folder &folder=folderMap[folderId];
        // LibMTP seems to reset parent_id to 0 - but we NEED the correct value for encodePath
        meta->parent_id=folderId;
        if (copyCover) {
            QMap<uint32_t, File>::ConstIterator it=folder.covers.constBegin();
            QMap<uint32_t, File>::ConstIterator end=folder.covers.constEnd();

            for (; it!=end; ++it) {
                if (it.value().name==opts.coverName) {
                    copiedCover=true;
                    copyCover=false;
                    break;
                }
            }
        }
        // Send cover, as a plain file...
        if (copyCover) {
            QString srcFile;
            Covers::Image coverImage=Covers::self()->getImage(s);
            QTemporaryFile *temp=0;
            if (!coverImage.img.isNull() && !coverImage.fileName.isEmpty()) {
                if (opts.coverMaxSize && (coverImage.img.width()>(int)opts.coverMaxSize || coverImage.img.height()>(int)opts.coverMaxSize)) {
                    temp=saveImageToTemp(coverImage.img.scaled(QSize(opts.coverMaxSize, opts.coverMaxSize), Qt::KeepAspectRatio, Qt::SmoothTransformation), opts.coverName);
                } else if (!coverImage.fileName.endsWith(".jpg", Qt::CaseInsensitive) || !QFile::exists(coverImage.fileName)) {
                    temp=saveImageToTemp(coverImage.img, opts.coverName);
                } else {
                    srcFile=coverImage.fileName;
                }
                if (temp) {
                    srcFile=temp->fileName();
                }
            }

            if (!srcFile.isEmpty()) {
                LIBMTP_file_t *fileMeta=LIBMTP_new_file_t();
                fileMeta->item_id=0;
                fileMeta->parent_id=folderId;
                fileMeta->storage_id=store.id;
                fileMeta->filename=createString(opts.coverName);
                struct stat statBuf;
                if (0==stat(QFile::encodeName(srcFile).constData(), &statBuf)) {
                    fileMeta->filesize=statBuf.st_size;
                    fileMeta->modificationdate=statBuf.st_mtime;
                }
                meta->filetype=mtpFileType(opts.coverName);

                if (0==LIBMTP_Send_File_From_File(device, srcFile.toUtf8(), fileMeta, 0, 0)) {
                    folder.covers.insert(fileMeta->item_id, File(opts.coverName, statBuf.st_size, fileMeta->item_id));
                    copiedCover=true;
                }
                LIBMTP_destroy_file_t(fileMeta);
            }
            if (temp) {
                temp->remove();
                delete temp;
            }
        }
        folder.files.insert(meta->item_id, File(destName, meta->filesize, meta->item_id));
        updateStorage();
    }
    emit putSongStatus(status,
                       meta ? encodePath(meta, destName, storage.count()>1 ? store.description : QString()) : QString(),
                       fixedVa, copiedCover);
    if (meta) {
        LIBMTP_destroy_track_t(meta);
    }
}

MtpConnection::File MtpConnection::getCoverDetils(const Song &s)
{
    File cover;
    Path path=decodePath(s.file);
    if (path.ok() && folderMap.contains(path.parent) && !folderMap[path.parent].covers.isEmpty()) {
        QMap<uint32_t, File> &covers=folderMap[path.parent].covers;
        QMap<uint32_t, File>::ConstIterator it=covers.constBegin();
        QMap<uint32_t, File>::ConstIterator end=covers.constEnd();

        for (; it!=end; ++it) {
            if (it.value().size>cover.size) {
                cover=it.value();
            }
        }
    }

    return cover;
}

void MtpConnection::getSong(const Song &song, const QString &dest, bool fixVa, bool copyCover)
{
    bool copiedSong=device && 0==LIBMTP_Get_File_To_File(device, song.id, dest.toUtf8(), &progressMonitor, this);
    bool copiedCover=false;

    if (copiedSong && !abortRequested && copyCover) {
        QString destDir=Utils::getDir(dest);

        if (QDir(destDir).entryList(QStringList() << QLatin1String("*.jpg") << QLatin1String("*.png"), QDir::Files|QDir::Readable).isEmpty()) {
            File cover=getCoverDetils(song);

            if (0!=cover.id) {
                QString mpdCover=MPDConnection::self()->getDetails().coverName;
                if (mpdCover.isEmpty()) {
                    mpdCover="cover";
                }
                QString fileName=QString(destDir+mpdCover+(cover.name.endsWith(".jpg", Qt::CaseInsensitive) ? ".jpg" : ".png"));
                QByteArray fileNameUtf8=fileName.toUtf8();
                copiedCover=0==LIBMTP_Get_File_To_File(device, cover.id, fileNameUtf8.constData(), 0, 0);
                if (copiedCover) {
                    Utils::setFilePerms(fileName);
                }
            }
        }
    }

    if (copiedSong && fixVa && !abortRequested) {
        Song s(song);
        Device::fixVariousArtists(dest, s, false);
    }
    emit getSongStatus(copiedSong, copiedCover);
}

void MtpConnection::delSong(const Song &song)
{
    Path path=decodePath(song.file);
    bool deletedSong=device && path.ok() && 0==LIBMTP_Delete_Object(device, path.id);
    if (deletedSong) {
        folderMap[path.parent].files.remove(path.id);
        #ifdef MTP_CLEAN_ALBUMS
        // Remove track from album. Remove album (and cover) if no tracks.
        LIBMTP_album_t *album=getAlbum(song);
        if (album) {
            if (0==album->no_tracks || (1==album->no_tracks && album->tracks[0]==(uint32_t)song.id)) {
                LIBMTP_Delete_Object(device, album->album_id);
                updateAlbums();
            } else if (album->no_tracks>1) {
                // Remove track from album...
                uint32_t *tracks = (uint32_t *)malloc((album->no_tracks-1)*sizeof(uint32_t));
                if (tracks) {
                    bool found=false;
                    for (uint32_t i=0, j=0; i<album->no_tracks && j<(album->no_tracks-1); ++i) {
                        if (album->tracks[i]!=(uint32_t)song.id) {
                            tracks[j++]=album->tracks[i];
                        } else {
                            found=true;
                        }
                    }
                    if (found) {
                        album->no_tracks--;
                        free(album->tracks);
                        album->tracks = tracks;
                        LIBMTP_Update_Album(device, album);
                    } else {
                        free(tracks);
                    }
                }
            }
        }
        #endif
        updateStorage();
    }
    emit delSongStatus(deletedSong);
}

bool MtpConnection::removeFolder(uint32_t folderId)
{
    QMap<uint32_t, Folder>::iterator folder=folderMap.find(folderId);
    if (folderMap.end()!=folder && (*folder).folders.isEmpty() && (*folder).files.isEmpty()) {
        // Delete any cover files...
        QList<uint32_t> toRemove=(*folder).covers.keys();
        foreach (uint32_t cover, toRemove) {
            if (0==LIBMTP_Delete_Object(device, cover)) {
                (*folder).covers.remove(cover);
            }
        }

        // Delete folder, if it is now empty...
        if ((*folder).covers.isEmpty() && 0==LIBMTP_Delete_Object(device, folderId)) {
            if (folderMap.contains((*folder).parentId)) {
                folderMap[(*folder).parentId].folders.remove(folderId);
            }
            folderMap.remove(folderId);
            return true;
        }
    }
    return false;
}

void MtpConnection::cleanDirs(const QSet<QString> &dirs)
{
    foreach (const QString &d, dirs) {
        Path path=decodePath(d);
        Storage &store=getStorage(path.storage);
        if (0==store.musicFolderId) {
            continue;
        }
        uint32_t folderId=path.parent;
        while (0!=folderId && folderId!=store.musicFolderId) {
            QMap<uint32_t, Folder>::iterator it=folderMap.find(folderId);
            if (it!=folderMap.end()) {
                if (removeFolder(folderId)) {
                    folderId=(*it).parentId;
                    folderMap.erase(it);
                } else {
                    break;
                }
            } else {
                break;
            }
        }
    }

    updateStorage();
    emit cleanDirsStatus(true);
}

void MtpConnection::getCover(const Song &song)
{
    File c=getCoverDetils(song);

    if (0!=c.id) {
        QByteArray data;
        if (0==LIBMTP_Get_File_To_Handler(device, c.id, fileReceiver, &data, 0, 0)) {
            QImage img;
            if (img.loadFromData(data)) {
                emit cover(song, img);
            }
        }
    }
}

void MtpConnection::stop()
{
    if (thread) {
        disconnectFromDevice(false);
        thread->stop();
        thread=0;
    }
}

#ifdef MTP_CLEAN_ALBUMS
void MtpConnection::updateAlbums()
{
    while (albums) {
        LIBMTP_album_t *album=albums;
        albums=albums->next;
        LIBMTP_destroy_album_t(album);
    }
    albums=LIBMTP_Get_Album_List(device);
}

LIBMTP_album_t * MtpConnection::getAlbum(const Song &song)
{
    LIBMTP_album_t *al=albums;
    LIBMTP_album_t *possible=0;

    while (al) {
        if (QString::fromUtf8(al->name)==song.album) {
            // For some reason, MTP sometimes leaves blank albums behind.
            // So, when looking for an album if we find one with the same name, but no tracks - then save this as a possibility...
            if (0==al->no_tracks) {
                QString aa(QString::fromUtf8(al->artist));
                if (aa.isEmpty() || aa==song.albumArtist()) {
                    possible=al;
                }
            } else {
                for (uint32_t i=0; i<al->no_tracks; ++i) {
                    if (al->tracks[i]==(uint32_t)song.id) {
                        return al;
                    }
                }
            }
        }
        al=al->next;
    }

    return possible;
}
#endif

void MtpConnection::destroyData()
{
    folderMap.clear();
    if (library) {
        delete library;
        library=0;
    }
    
    #ifdef MTP_CLEAN_ALBUMS
    while (albums) {
        LIBMTP_album_t *album=albums;
        albums=albums->next;
        LIBMTP_destroy_album_t(album);
    }
    #endif
}

QString cfgKey(Solid::Device &dev, const QString &serial)
{
    QString key=QLatin1String("MTP-")+dev.vendor()+QChar('-')+dev.product()+QChar('-')+serial;
    key.replace('/', '_');
    return key;
}

MtpDevice::MtpDevice(MusicModel *m, Solid::Device &dev)
    : Device(m, dev,
             #ifdef MTP_FAKE_ALBUMARTIST_SUPPORT
             true
             #else
             false
             #endif
             )
    , pmp(dev.as<Solid::PortableMediaPlayer>())
    , tempFile(0)
    , mtpUpdating(false)
{
    static bool registeredTypes=false;
    if (!registeredTypes) {
        qRegisterMetaType<QSet<QString> >("QSet<QString>");
        qRegisterMetaType<DeviceOptions >("DeviceOptions");
        registeredTypes=true;
    }

    Solid::GenericInterface *iface = dev.as<Solid::GenericInterface>();
    unsigned int busNum(0);
    unsigned int devNum(0);
    if (iface) {
        QMap<QString, QVariant> properties = iface->allProperties();
        busNum = properties.value(QLatin1String("BUSNUM")).toInt();
        devNum = properties.value(QLatin1String("DEVNUM")).toInt();
    }

    connection=new MtpConnection(busNum, devNum, supportsAlbumArtistTag());
    connect(this, SIGNAL(updateLibrary(const DeviceOptions &)), connection, SLOT(updateLibrary(const DeviceOptions &)));
    connect(connection, SIGNAL(libraryUpdated()), this, SLOT(libraryUpdated()));
    connect(connection, SIGNAL(progress(int)), this, SLOT(emitProgress(int)));
    connect(this, SIGNAL(putSong(const Song &, bool, const DeviceOptions &, bool, bool)), connection, SLOT(putSong(const Song &, bool, const DeviceOptions &, bool, bool)));
    connect(connection, SIGNAL(putSongStatus(int, const QString &, bool, bool)), this, SLOT(putSongStatus(int, const QString &, bool, bool)));
    connect(this, SIGNAL(getSong(const Song &, const QString &, bool, bool)), connection, SLOT(getSong(const Song &, const QString &, bool, bool)));
    connect(connection, SIGNAL(getSongStatus(bool, bool)), this, SLOT(getSongStatus(bool, bool)));
    connect(this, SIGNAL(delSong(const Song &)), connection, SLOT(delSong(const Song &)));
    connect(connection, SIGNAL(delSongStatus(bool)), this, SLOT(delSongStatus(bool)));
    connect(this, SIGNAL(cleanMusicDirs(const QSet<QString> &)), connection, SLOT(cleanDirs(const QSet<QString> &)));
    connect(this, SIGNAL(getCover(const Song &)), connection, SLOT(getCover(const Song &)));
    connect(connection, SIGNAL(cleanDirsStatus(bool)), this, SLOT(cleanDirsStatus(bool)));
    connect(connection, SIGNAL(statusMessage(const QString &)), this, SLOT(setStatusMessage(const QString &)));
    connect(connection, SIGNAL(deviceDetails(const QString &)), this, SLOT(deviceDetails(const QString &)));
    connect(connection, SIGNAL(updatePercentage(int)), this, SLOT(updatePercentage(int)));
    connect(connection, SIGNAL(cover(const Song &, const QImage &)), this, SIGNAL(cover(const Song &, const QImage &)));
    opts.fixVariousArtists=false;
    opts.coverName=constMtpDefaultCover;
    QTimer::singleShot(0, this, SLOT(rescan(bool)));
    defaultName=data();
    if (!opts.name.isEmpty()) {
        setData(opts.name);
    }
}

MtpDevice::~MtpDevice()
{
    stop();
}

void MtpDevice::deviceDetails(const QString &s)
{
    if ((s!=serial || serial.isEmpty()) && solidDev.isValid()) {
        serial=s;
        QString configKey=cfgKey(solidDev, serial);
        opts.load(configKey);
        configured=Configuration().hasGroup(configKey);
        if (!opts.name.isEmpty() && opts.name!=defaultName) {
            setData(opts.name);
            emit renamed();
        }
    }
}

bool MtpDevice::isConnected() const
{
    return solidDev.isValid() && pmp && pmp->isValid() && connection->isConnected();
}

void MtpDevice::stop()
{
    abortJob();
    deleteTemp();
    if (connection) {
        disconnect(connection, SIGNAL(libraryUpdated()), this, SLOT(libraryUpdated()));
        disconnect(connection, SIGNAL(progress(int)), this, SLOT(emitProgress(int)));
        disconnect(connection, SIGNAL(putSongStatus(int, const QString &, bool, bool)), this, SLOT(putSongStatus(int, const QString &, bool, bool)));
        disconnect(connection, SIGNAL(getSongStatus(bool, bool)), this, SLOT(getSongStatus(bool, bool)));
        disconnect(connection, SIGNAL(delSongStatus(bool)), this, SLOT(delSongStatus(bool)));
        disconnect(connection, SIGNAL(cleanDirsStatus(bool)), this, SLOT(cleanDirsStatus(bool)));
        disconnect(connection, SIGNAL(statusMessage(const QString &)), this, SLOT(setStatusMessage(const QString &)));
        disconnect(connection, SIGNAL(deviceDetails(const QString &)), this, SLOT(deviceDetails(const QString &)));
        disconnect(connection, SIGNAL(updatePercentage(int)), this, SLOT(updatePercentage(int)));
        disconnect(connection, SIGNAL(cover(const Song &, const QImage &)), this, SIGNAL(cover(const Song &, const QImage &)));
        metaObject()->invokeMethod(connection, "stop", Qt::QueuedConnection);
        connection->deleteLater();
        connection=0;
    }
}

void MtpDevice::configure(QWidget *parent)
{
    if (!isIdle()) {
        return;
    }

    DevicePropertiesDialog *dlg=new DevicePropertiesDialog(parent);
    connect(dlg, SIGNAL(updatedSettings(const QString &, const DeviceOptions &)), SLOT(saveProperties(const QString &, const DeviceOptions &)));
    if (!configured) {
        connect(dlg, SIGNAL(cancelled()), SLOT(saveProperties()));
    }
    DeviceOptions o=opts;
    if (o.name.isEmpty()) {
        o.name=data();
    }
    dlg->show(QString(), o, connection->getStorageList(), DevicePropertiesWidget::Prop_Name|DevicePropertiesWidget::Prop_CoversAll|DevicePropertiesWidget::Prop_Va|DevicePropertiesWidget::Prop_Transcoder);
}

void MtpDevice::rescan(bool full)
{
    Q_UNUSED(full)
    if (mtpUpdating || !solidDev.isValid()) {
        return;
    }
    if (childCount()) {
        update=new MusicLibraryItemRoot();
        applyUpdate();
    }
    mtpUpdating=true;
    emit updating(solidDev.udi(), true);
    emit updateLibrary(opts);
}

void MtpDevice::addSong(const Song &s, bool overwrite, bool copyCover)
{
    requestAbort(false);
    if (!isConnected()) {
        emit actionStatus(NotConnected);
        return;
    }

    needToFixVa=opts.fixVariousArtists && s.isVariousArtists();

    if (!overwrite) {
        Song check=s;

        if (needToFixVa) {
            Device::fixVariousArtists(QString(), check, true);
        }
        if (songExists(check)) {
            emit actionStatus(SongExists);
            return;
        }
    }

    if (!QFile::exists(s.file)) {
        emit actionStatus(SourceFileDoesNotExist);
        return;
    }
    currentSong=s;
    if (!opts.transcoderCodec.isEmpty()) {
        Encoders::Encoder encoder=Encoders::getEncoder(opts.transcoderCodec);
        if (encoder.codec.isEmpty()) {
            emit actionStatus(CodecNotAvailable);
            return;
        }

        if (!opts.transcoderWhenDifferent || encoder.isDifferent(s.file)) {
            deleteTemp();
            tempFile=new QTemporaryFile(QDir::tempPath()+"/cantata_XXXXXX."+encoder.extension);
            tempFile->setAutoRemove(false);

            if (!tempFile->open()) {
                deleteTemp();
                emit actionStatus(FailedToCreateTempFile);
                return;
            }
            QString destFile=tempFile->fileName();
            tempFile->close();
            if (QFile::exists(destFile)) {
                QFile::remove(destFile);
            }
            transcoding=true;
            TranscodingJob *job=new TranscodingJob(encoder, opts.transcoderValue, s.file, destFile);
            job->setProperty("overwrite", overwrite);
            job->setProperty("copyCover", copyCover);
            connect(job, SIGNAL(result(int)), SLOT(transcodeSongResult(int)));
            connect(job, SIGNAL(percent(int)), SLOT(transcodePercent(int)));
            job->start();
            currentSong.file=destFile;
            return;
        }
    }
    transcoding=false;
    emit putSong(currentSong, needToFixVa, opts, overwrite, copyCover);
}

void MtpDevice::copySongTo(const Song &s, const QString &musicPath, bool overwrite, bool copyCover)
{
    requestAbort(false);
    transcoding=false;
    if (!isConnected()) {
        emit actionStatus(NotConnected);
        return;
    }

    needToFixVa=opts.fixVariousArtists && s.isVariousArtists();

    if (!overwrite) {
        Song check=s;

        if (needToFixVa) {
            Device::fixVariousArtists(QString(), check, false);
        }
        if (MusicLibraryModel::self()->songExists(check)) {
            emit actionStatus(SongExists);
            return;
        }
    }

    if (!songExists(s)) {
        emit actionStatus(SongDoesNotExist);
        return;
    }

    QString baseDir=MPDConnection::self()->getDetails().dir;
    currentDestFile=baseDir+musicPath;
    QDir dir(Utils::getDir(currentDestFile));
    if (!dir.exists() && !Utils::createWorldReadableDir(dir.absolutePath(), baseDir)) {
        emit actionStatus(DirCreationFaild);
        return;
    }

    currentSong=s;
    emit getSong(s, currentDestFile, needToFixVa, copyCover);
}

void MtpDevice::removeSong(const Song &s)
{
    requestAbort(false);
    if (!isConnected()) {
        emit actionStatus(NotConnected);
        return;
    }

    if (!songExists(s)) {
        emit actionStatus(SongDoesNotExist);
        return;
    }

    currentSong=s;
    emit delSong(s);
}

void MtpDevice::cleanDirs(const QSet<QString> &dirs)
{
    requestAbort(false);
    if (!isConnected()) {
        emit actionStatus(NotConnected);
        return;
    }
    emit cleanMusicDirs(dirs);
}

void MtpDevice::requestCover(const Song &song)
{
    requestAbort(false);
    if (isConnected()) {
        emit getCover(song);
    }
}

void MtpDevice::putSongStatus(int status, const QString &file, bool fixedVa, bool copiedCover)
{
    deleteTemp();
    if (jobAbortRequested) {
        return;
    }
    if (Ok!=status) {
        emit actionStatus(status);
    } else {
        currentSong.file=file;
        if (needToFixVa && fixedVa) {
            currentSong.fixVariousArtists();
        }
        #ifndef MTP_FAKE_ALBUMARTIST_SUPPORT
        else if (!opts.fixVariousArtists) { // TODO: ALBUMARTIST: Remove when libMTP supports album artist!
            currentSong.albumartist=currentSong.artist;
        }
        #endif
        addSongToList(currentSong);
        emit actionStatus(Ok, copiedCover);
    }
}

void MtpDevice::transcodeSongResult(int status)
{
    TranscodingJob *job=qobject_cast<TranscodingJob *>(sender());
    if (!job) {
        return;
    }
    FileJob::finished(job);
    if (jobAbortRequested) {
        deleteTemp();
        return;
    }
    if (Ok!=status) {
        emit actionStatus(status);
    } else {
        emit putSong(currentSong, needToFixVa, opts, job->property("overwrite").toBool(), job->property("copyCover").toBool());
    }
}

void MtpDevice::transcodePercent(int percent)
{
    if (jobAbortRequested) {
        FileJob *job=qobject_cast<FileJob *>(sender());
        if (job) {
            job->stop();
        }
        return;
    }
    emit progress(percent/2);
}

void MtpDevice::emitProgress(int percent)
{
    if (jobAbortRequested) {
        return;
    }
    emit progress(transcoding ? (50+(percent/2)) : percent);
}

void MtpDevice::getSongStatus(bool ok, bool copiedCover)
{
    if (jobAbortRequested) {
        return;
    }
    if (!ok) {
        emit actionStatus(Failed);
    } else {
        currentSong.file=currentDestFile.mid(MPDConnection::self()->getDetails().dir.length());
        QString origPath;
        if (MPDConnection::self()->isMopdidy()) {
            origPath=currentSong.file;
            currentSong.file=Song::encodePath(currentSong.file);
        }
        if (needToFixVa) {
            currentSong.revertVariousArtists();
        }
        Utils::setFilePerms(currentDestFile);
        MusicLibraryModel::self()->addSongToList(currentSong);
        DirViewModel::self()->addFileToList(origPath.isEmpty() ? currentSong.file : origPath,
                                            origPath.isEmpty() ? QString() : currentSong.file);
        emit actionStatus(Ok, copiedCover);
    }
}

void MtpDevice::delSongStatus(bool ok)
{
    if (jobAbortRequested) {
        return;
    }
    if (!ok) {
        emit actionStatus(Failed);
    } else {
        removeSongFromList(currentSong);
        emit actionStatus(Ok);
    }
}

void MtpDevice::cleanDirsStatus(bool ok)
{
    emit actionStatus(ok ? Ok : Failed);
}

double MtpDevice::usedCapacity()
{
    if (!isConnected()) {
        return -1.0;
    }

    return connection->capacity()>0 ? (connection->usedSpace()*1.0)/(connection->capacity()*1.0) : -1.0;
}

QString MtpDevice::capacityString()
{
    if (!isConnected()) {
        return i18n("Not Connected");
    }

    return i18n("%1 free", Utils::formatByteSize(connection->capacity()-connection->usedSpace()));
}

qint64 MtpDevice::freeSpace()
{
    if (!isConnected()) {
        return 0;
    }

    return connection->capacity()-connection->usedSpace();
}

void MtpDevice::libraryUpdated()
{
    if (update) {
        delete update;
    }
    update=connection->takeLibrary();
    setStatusMessage(QString());
    emit updating(id(), false);
    mtpUpdating=false;
}

void MtpDevice::saveProperties(const QString &, const DeviceOptions &newOpts)
{
    if (configured && opts==newOpts) {
        return;
    }

    QString newName=newOpts.name.isEmpty() ? defaultName : newOpts.name;
    bool diffName=opts.name!=newName;
    opts=newOpts;
    if (diffName) {
        setData(newName);
    }
    saveProperties();
    if (diffName) {
        emit renamed();
    }
}

void MtpDevice::saveProperties()
{
    if (solidDev.isValid()) {
        configured=true;
        opts.save(cfgKey(solidDev, serial), false, true, false); // Dont save fileame scheme - cant be changed!
        emit configurationChanged();
    }
}

void MtpDevice::saveOptions()
{
    if (solidDev.isValid()) {
        opts.save(cfgKey(solidDev, serial), false, true, false); // Dont save fileame scheme - cant be changed!
    }
}

void MtpDevice::deleteTemp()
{
    if (tempFile) {
        tempFile->remove();
        delete tempFile;
        tempFile=0;
    }
}
