/*
 * Cantata
 *
 * Copyright (c) 2011-2022 Craig Drummond <craig.p.drummond@gmail.com>
 *
 */
/*
 * Copyright (c) 2008 Sander Knopper (sander AT knopper DOT tk) and
 *                    Roeland Douma (roeland AT rullzer DOT com)
 *
 * This file is part of QtMPC.
 *
 * QtMPC 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.
 *
 * QtMPC 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 QtMPC.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "mpdconnection.h"
#include "mpdparseutils.h"
#include "models/streamsmodel.h"
#ifdef ENABLE_SIMPLE_MPD_SUPPORT
#include "mpduser.h"
#endif
#include "gui/settings.h"
#include "support/globalstatic.h"
#include "support/configuration.h"
#include <QStringList>
#include <QTimer>
#include <QDir>
#include <QHostInfo>
#include <QDate>
#include <QDateTime>
#include <QPropertyAnimation>
#include <QCoreApplication>
#include <QUdpSocket>
#include <complex>
#include "support/thread.h"
#include "cuefile.h"
#if defined Q_OS_LINUX && defined QT_QTDBUS_FOUND
#include "dbus/powermanagement.h"
#elif defined Q_OS_MAC && defined IOKIT_FOUND
#include "mac/powermanagement.h"
#elif defined Q_OS_WIN
#define sscanf sscanf_s
#endif
#include <algorithm>
#include <QDebug>
static bool debugEnabled=false;
#define DBUG if (debugEnabled) qWarning() << "MPDConnection" << QThread::currentThreadId()
void MPDConnection::enableDebug()
{
    debugEnabled=true;
}

// Uncomment the following to report error strings in MPDStatus to the UI
// ...disabled, as stickers (for ratings) can cause lots of errors to be reported - and these all need clearing, etc.
// #define REPORT_MPD_ERRORS
static const int constSocketCommsTimeout=2000;
static const int constMaxReadAttempts=4;
static const int constMaxFilesPerAddCommand=2000;
static const int constConnTimer=5000;

static const QByteArray constOkValue("OK");
static const QByteArray constOkMpdValue("OK MPD");
static const QByteArray constOkNlValue("OK\n");
static const QByteArray constAckValue("ACK");
static const QByteArray constIdleChangedKey("changed: ");
static const QByteArray constIdleDbValue("database");
static const QByteArray constIdleUpdateValue("update");
static const QByteArray constIdleStoredPlaylistValue("stored_playlist");
static const QByteArray constIdlePlaylistValue("playlist");
static const QByteArray constIdlePlayerValue("player");
static const QByteArray constIdleMixerValue("mixer");
static const QByteArray constIdleOptionsValue("options");
static const QByteArray constIdleOutputValue("output");
static const QByteArray constIdlePartitionValue("partition");
static const QByteArray constIdleStickerValue("sticker");
static const QByteArray constIdleSubscriptionValue("subscription");
static const QByteArray constIdleMessageValue("message");
static const QByteArray constDynamicIn("cantata-dynamic-in");
static const QByteArray constDynamicOut("cantata-dynamic-out");
static const QByteArray constRatingSticker("rating");

static inline int socketTimeout(int dataSize)
{
    static const int constDataBlock=256;
    return ((dataSize/constDataBlock)+((dataSize%constDataBlock) ? 1 : 0))*constSocketCommsTimeout;
}

static QByteArray log(const QByteArray &data)
{
    if (data.length()<256) {
        return data;
    } else {
        return data.left(96) + "... ..." + data.right(96) + " (" + QByteArray::number(data.length()) + " bytes)";
    }
}

GLOBAL_STATIC(MPDConnection, instance)

const QString MPDConnection::constModifiedSince=QLatin1String("modified-since");
const int MPDConnection::constMaxPqChanges=1000;
const QString MPDConnection::constStreamsPlayListName=QLatin1String("[Radio Streams]");
const QString MPDConnection::constPlaylistPrefix=QLatin1String("playlist:");
const QString MPDConnection::constDirPrefix=QLatin1String("dir:");

QByteArray MPDConnection::quote(int val)
{
    return '\"'+QByteArray::number(val)+'\"';
}

QByteArray MPDConnection::encodeName(const QString &name)
{
    return '\"'+name.toUtf8().replace("\\", "\\\\").replace("\"", "\\\"")+'\"';
}

static QByteArray readFromSocket(MpdSocket &socket, int timeout=constSocketCommsTimeout)
{
    QByteArray data;
    int attempt=0;
    while (QAbstractSocket::ConnectedState==socket.state()) {
        while (0==socket.bytesAvailable() && QAbstractSocket::ConnectedState==socket.state()) {
            DBUG << (void *)(&socket) << "Waiting for read data, attempt" << attempt;
            if (socket.waitForReadyRead(timeout)) {
                break;
            }
            DBUG << (void *)(&socket) << "Wait for read failed - " << socket.errorString();
            if (++attempt>=constMaxReadAttempts) {
                DBUG << "ERROR: Timedout waiting for response";
                socket.close();
                return QByteArray();
            }
        }

        data.append(socket.readAll());

        if (data.endsWith(constOkNlValue) || data.startsWith(constOkValue) || data.startsWith(constAckValue)) {
            break;
        }
    }
    DBUG << (void *)(&socket) << "Read:" << log(data) << ", socket state:" << socket.state();

    return data;
}

static MPDConnection::Response readReply(MpdSocket &socket, int timeout=constSocketCommsTimeout)
{
    QByteArray data = readFromSocket(socket, timeout);
    return MPDConnection::Response(data.endsWith(constOkNlValue), data);
}

MPDConnection::Response::Response(bool o, const QByteArray &d)
    : ok(o)
    , data(d)
{
}

QString MPDConnection::Response::getError(const QByteArray &command)
{
    if (ok || data.isEmpty()) {
        return QString();
    }

    if (!ok && data.size()>0) {
        int cmdEnd=data.indexOf("} ");
        if (-1==cmdEnd) {
            return tr("Unknown")+QLatin1String(" (")+command+QLatin1Char(')');
        } else {
            cmdEnd+=2;
            QString rv=data.mid(cmdEnd, data.length()-(data.endsWith('\n') ? cmdEnd+1 : cmdEnd));
            if (data.contains("{listplaylists}")) {
                // NOTE: NOT translated, as refers to config item
                return QLatin1String("playlist_directory - ")+rv;
            }

            // If we are reporting a stream error, remove any stream name added by Cantata...
            int start=rv.indexOf(QLatin1String("http://"));
            if (start>0) {
                int pos=rv.indexOf(QChar('#'), start+6);
                if (-1!=pos) {
                    rv=rv.left(pos);
                }
            }

            return rv;
        }
    }
    return data;
}

MPDConnectionDetails::MPDConnectionDetails()
    : port(6600)
    , dirReadable(false)
    , applyReplayGain(true)
    , allowLocalStreaming(true)
    , autoUpdate(false)
{
}

QString MPDConnectionDetails::getName() const
{
    #ifdef ENABLE_SIMPLE_MPD_SUPPORT
    return name.isEmpty() ? QObject::tr("Default") : (name==MPDUser::constName ? MPDUser::translatedName() : name);
    #else
    return name.isEmpty() ? QObject::tr("Default") : name;
    #endif
}

QString MPDConnectionDetails::description() const
{
    if (hostname.isEmpty()) {
        return getName();
    } else if (hostname.startsWith('/') || hostname.startsWith('~')) {
        return getName();
    } else {
        return QObject::tr("\"%1\" (%2:%3)", "name (host:port)").arg(getName()).arg(hostname).arg(QString::number(port));
    }
}

MPDConnectionDetails & MPDConnectionDetails::operator=(const MPDConnectionDetails &o)
{
    name=o.name;
    hostname=o.hostname;
    port=o.port;
    password=o.password;
    partition=o.partition;
    dir=o.dir;
    dirReadable=o.dirReadable;
    #ifdef ENABLE_HTTP_STREAM_PLAYBACK
    streamUrl=o.streamUrl;
    #endif
    replayGain=o.replayGain;
    applyReplayGain=o.applyReplayGain;
    allowLocalStreaming=o.allowLocalStreaming;
    autoUpdate=o.autoUpdate;
    return *this;
}

void MPDConnectionDetails::setDirReadable()
{
    dirReadable=Utils::isDirReadable(dir);
}

MPDConnection::MPDConnection()
    : isInitialConnect(true)
    , thread(nullptr)
    , ver(0)
    , canUseStickers(false)
    , sock(this)
    , idleSocket(this)
    , lastStatusPlayQueueVersion(0)
    , lastUpdatePlayQueueVersion(0)
    , state(State_Blank)
    , isListingMusic(false)
    , reconnectTimer(nullptr)
    , reconnectStart(0)
    , stopAfterCurrent(false)
    , currentSongId(-1)
    , songPos(0)
    , unmuteVol(-1)
    , isUpdatingDb(false)
    , volumeFade(nullptr)
    , fadeDuration(0)
    , restoreVolume(-1)
{
    qRegisterMetaType<time_t>("time_t");
    qRegisterMetaType<Song>("Song");
    qRegisterMetaType<Partition>("Partition");
    qRegisterMetaType<Output>("Output");
    qRegisterMetaType<Playlist>("Playlist");
    qRegisterMetaType<QList<Song> >("QList<Song>");
    qRegisterMetaType<QList<Partition> >("QList<Partition>");
    qRegisterMetaType<QList<Output> >("QList<Output>");
    qRegisterMetaType<QList<Playlist> >("QList<Playlist>");
    qRegisterMetaType<QList<quint32> >("QList<quint32>");
    qRegisterMetaType<QList<qint32> >("QList<qint32>");
    qRegisterMetaType<QList<quint8> >("QList<quint8>");
    qRegisterMetaType<QSet<qint32> >("QSet<qint32>");
    qRegisterMetaType<QSet<QString> >("QSet<QString>");
    qRegisterMetaType<QAbstractSocket::SocketState >("QAbstractSocket::SocketState");
    qRegisterMetaType<MPDStatsValues>("MPDStatsValues");
    qRegisterMetaType<MPDStatusValues>("MPDStatusValues");
    qRegisterMetaType<MPDConnectionDetails>("MPDConnectionDetails");
    qRegisterMetaType<QMap<qint32, quint8> >("QMap<qint32, quint8>");
    qRegisterMetaType<Stream>("Stream");
    qRegisterMetaType<QList<Stream> >("QList<Stream>");
    #if (defined Q_OS_LINUX && defined QT_QTDBUS_FOUND) || (defined Q_OS_MAC && defined IOKIT_FOUND)
    connect(PowerManagement::self(), SIGNAL(resuming()), this, SLOT(reconnect()));
    #endif
    MPDParseUtils::setSingleTracksFolders(Utils::listToSet(Configuration().get("singleTracksFolders", QStringList())));
}

MPDConnection::~MPDConnection()
{
    if (State_Connected==state && fadingVolume()) {
        sendCommand("stop");
        stopVolumeFade();
    }
//     disconnect(&sock, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
    disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
    disconnect(&idleSocket, SIGNAL(readyRead()), this, SLOT(idleDataReady()));
    sock.disconnectFromHost();
    idleSocket.disconnectFromHost();
}

void MPDConnection::start()
{
    if (!thread) {
        thread=new Thread(metaObject()->className());
        connTimer=thread->createTimer(this);
        connTimer->setSingleShot(false);
        moveToThread(thread);
        connect(thread, SIGNAL(finished()), connTimer, SLOT(stop()));
        connect(connTimer, SIGNAL(timeout()), SLOT(getStatus()));
        thread->start();
    }
}

void MPDConnection::stop()
{
    stopPlaying();
    #ifdef ENABLE_SIMPLE_MPD_SUPPORT
    if (details.name==MPDUser::constName && Settings::self()->stopOnExit()) {
        MPDUser::self()->stop();
    }
    #endif

    if (thread) {
        thread->deleteTimer(connTimer);
        connTimer=nullptr;
        thread->stop();
        thread=nullptr;
    }
}

bool MPDConnection::localFilePlaybackSupported() const
{
    return details.isLocal() ||
           (ver>=CANTATA_MAKE_VERSION(0, 19, 0) && /*handlers.contains(QLatin1String("file")) &&*/
           (QLatin1String("127.0.0.1")==details.hostname || QLatin1String("localhost")==details.hostname));
}

MPDConnection::ConnectionReturn MPDConnection::connectToMPD(MpdSocket &socket, bool enableIdle)
{
    if (QAbstractSocket::ConnectedState!=socket.state()) {
        DBUG << (void *)(&socket) << "Connecting" << (enableIdle ? "(idle)" : "(std)");
        if (details.isEmpty()) {
            DBUG << "no hostname and/or port supplied.";
            return Failed;
        }

        socket.connectToHost(details.hostname, details.port);
        if (socket.waitForConnected(constSocketCommsTimeout)) {
            DBUG << (void *)(&socket) << "established";
            QByteArray recvdata = readFromSocket(socket);

            if (recvdata.isEmpty()) {
                DBUG << (void *)(&socket) << "Couldn't connect";
                return Failed;
            }

            if (recvdata.startsWith(constOkMpdValue)) {
                DBUG << (void *)(&socket) << "Received identification string";
            }

            lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=0;
            playQueueIds.clear();
            emit cantataStreams(QList<Song>(), false);
            int min, maj, patch;
            if (3==sscanf(&(recvdata.constData()[7]), "%3d.%3d.%3d", &maj, &min, &patch)) {
                long v=((maj&0xFF)<<16)+((min&0xFF)<<8)+(patch&0xFF);
                if (v!=ver) {
                    ver=v;
                }
            }

            recvdata.clear();

            if (!details.password.isEmpty()) {
                DBUG << (void *)(&socket) << "setting password...";
                socket.write("password "+details.password.toUtf8()+'\n');
                socket.waitForBytesWritten(constSocketCommsTimeout);
                if (!readReply(socket).ok) {
                    DBUG << (void *)(&socket) << "password rejected";
                    socket.close();
                    return IncorrectPassword;
                }
            }

            if (!details.partition.isEmpty()) {
                DBUG << (void *)(&socket) << "setting partition...";
                socket.write("partition "+encodeName(details.partition)+'\n');
                socket.waitForBytesWritten(constSocketCommsTimeout);
                if (!readReply(socket).ok) {
                    DBUG << (void *)(&socket) << "partition rejected, staying on default";
                }
            }

            if (enableIdle) {
                dynamicId.clear();
                setupRemoteDynamic();
                connect(&socket, SIGNAL(readyRead()), this, SLOT(idleDataReady()), Qt::QueuedConnection);
                connect(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)), Qt::QueuedConnection);
                DBUG << (void *)(&socket) << "Enabling idle";
                socket.write("idle\n");
                socket.waitForBytesWritten();
            }
            return Success;
        } else {
            DBUG << (void *)(&socket) << "Couldn't connect - " << socket.errorString() << socket.error();
            return convertSocketCode(socket);
        }
    }

//     DBUG << "Already connected" << (enableIdle ? "(idle)" : "(std)");
    return Success;
}

MPDConnection::ConnectionReturn MPDConnection::convertSocketCode(MpdSocket &socket)
{
    switch (socket.error()) {
    case QAbstractSocket::ProxyAuthenticationRequiredError:
    case QAbstractSocket::ProxyConnectionRefusedError:
    case QAbstractSocket::ProxyConnectionClosedError:
    case QAbstractSocket::ProxyConnectionTimeoutError:
    case QAbstractSocket::ProxyNotFoundError:
    case QAbstractSocket::ProxyProtocolError:
        return MPDConnection::ProxyError;
    default:
        if (QNetworkProxy::DefaultProxy!=socket.proxyType() && QNetworkProxy::NoProxy!=socket.proxyType()) {
            return MPDConnection::ProxyError;
        }
        if (socket.errorString().contains(QLatin1String("proxy"), Qt::CaseInsensitive)) {
            return MPDConnection::ProxyError;
        }
        return MPDConnection::Failed;
    }
}

QString MPDConnection::errorString(ConnectionReturn status) const
{
    switch (status) {
    default:
    case Success:           return QString();
    case Failed:            return tr("Connection to %1 failed").arg(details.description());
    case ProxyError:        return tr("Connection to %1 failed - please check your proxy settings").arg(details.description());
    case IncorrectPassword: return tr("Connection to %1 failed - incorrect password").arg(details.description());
    }
}

MPDConnection::ConnectionReturn MPDConnection::connectToMPD()
{
    connTimer->stop();
    if (State_Connected==state && (QAbstractSocket::ConnectedState!=sock.state() || QAbstractSocket::ConnectedState!=idleSocket.state())) {
        DBUG << "Something has gone wrong with sockets, so disconnect";
        disconnectFromMPD();
    }
    #ifdef ENABLE_SIMPLE_MPD_SUPPORT
    int maxConnAttempts=MPDUser::constName==details.name ? 2 : 1;
    #else
    int maxConnAttempts=1;
    #endif
    ConnectionReturn status=Failed;
    for (int connAttempt=0; connAttempt<maxConnAttempts; ++connAttempt) {
        if (Success==(status=connectToMPD(sock)) && Success==(status=connectToMPD(idleSocket, true))) {
            state=State_Connected;
            emit socketAddress(sock.address());
        } else {
            disconnectFromMPD();
            state=State_Disconnected;
            #ifdef ENABLE_SIMPLE_MPD_SUPPORT
            if (0==connAttempt && MPDUser::constName==details.name) {
                DBUG << "Restart personal mpd";
                MPDUser::self()->stop();
                MPDUser::self()->start();
            }
            #endif
        }
    }
    connTimer->start(constConnTimer);
    return status;
}

void MPDConnection::disconnectFromMPD()
{
    DBUG << "disconnectFromMPD";
    connTimer->stop();
    disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
    disconnect(&idleSocket, SIGNAL(readyRead()), this, SLOT(idleDataReady()));
    if (QAbstractSocket::ConnectedState==sock.state()) {
        sock.disconnectFromHost();
    }
    if (QAbstractSocket::ConnectedState==idleSocket.state()) {
        idleSocket.disconnectFromHost();
    }
    sock.close();
    idleSocket.close();
    state=State_Disconnected;
    ver=0;
    playQueueIds.clear();
    streamIds.clear();
    lastStatusPlayQueueVersion=0;
    lastUpdatePlayQueueVersion=0;
    currentSongId=0;
    songPos=0;
    serverInfo.reset();
    isUpdatingDb=false;
    emit socketAddress(QString());
}

// This function is mainly intended to be called after the computer (laptop) has been 'resumed'
void MPDConnection::reconnect()
{
    if (reconnectTimer && reconnectTimer->isActive()) {
        return;
    }
    if (0==reconnectStart && isConnected()) {
        disconnectFromMPD();
    }

    if (isConnected()) { // Perhaps the user pressed a button which caused the reconnect???
        reconnectStart=0;
        return;
    }
    time_t now=time(nullptr);
    ConnectionReturn status=connectToMPD();
    switch (status) {
    case Success:
        // Issue #1041 - MPD does not seem to persist user/client made replaygain changes, so use the values read from Cantata's config.
        if (replaygainSupported() && details.applyReplayGain && !details.replayGain.isEmpty()) {
            sendCommand("replay_gain_mode "+details.replayGain.toLatin1());
        }
        serverInfo.detect();
        listPartitions();
        getStatus();
        getStats();
        getUrlHandlers();
        getTagTypes();
        getStickerSupport();
        playListInfo();
        outputs();
        reconnectStart=0;
        determineIfaceIp();
        emit stateChanged(true);
        break;
    case Failed:
        if (0==reconnectStart || std::abs(now-reconnectStart)<15) {
            if (0==reconnectStart) {
                reconnectStart=now;
            }
            if (!reconnectTimer) {
                reconnectTimer=new QTimer(this);
                reconnectTimer->setSingleShot(true);
                connect(reconnectTimer, SIGNAL(timeout()), this, SLOT(reconnect()), Qt::QueuedConnection);
            }
            if (std::abs(now-reconnectStart)>1) {
                emit info(tr("Connecting to %1").arg(details.description()));
            }
            reconnectTimer->start(500);
        } else {
            emit stateChanged(false);
            emit error(errorString(Failed), true);
            reconnectStart=0;
        }
        break;
    default:
        emit stateChanged(false);
        emit error(errorString(status), true);
        reconnectStart=0;
        break;
    }
}

void MPDConnection::setDetails(const MPDConnectionDetails &d)
{
    // Can't change connection whilst listing music collection...
    if (isListingMusic) {
        emit connectionNotChanged(details.name);
        return;
    }

    #ifdef ENABLE_SIMPLE_MPD_SUPPORT
    bool isUser=d.name==MPDUser::constName;
    const MPDConnectionDetails &det=isUser ? MPDUser::self()->details(true) : d;
    #else
    const MPDConnectionDetails &det=d;
    #endif
    bool changedDir=det.dir!=details.dir;
    bool diffName=det.name!=details.name;
    bool diffDetails=det!=details;
    #ifdef ENABLE_HTTP_STREAM_PLAYBACK
    bool diffStreamUrl=det.streamUrl!=details.streamUrl;
    #endif

    details=det;
    // Issue #1041 - If this is a user MPD, then the call to MPDUser::self()->details() will clear the replayGain setting
    // We can safely use that of the passed in details.
    details.replayGain=d.replayGain;

    if (diffDetails || State_Connected!=state) {
        emit connectionChanged(details);
        DBUG << "setDetails" << details.hostname << details.port << (details.password.isEmpty() ? false : true);
        if (State_Connected==state && fadingVolume()) {
            sendCommand("stop");
            stopVolumeFade();
        }
        disconnectFromMPD();
        DBUG << "call connectToMPD";
        unmuteVol=-1;
        toggleStopAfterCurrent(false);
        #ifdef ENABLE_SIMPLE_MPD_SUPPORT
        if (isUser) {
            MPDUser::self()->start();
        }
        #endif
        if (isUpdatingDb) {
            isUpdatingDb=false;
            emit updatedDatabase(); // Stop any spinners...
        }
        ConnectionReturn status=connectToMPD();
        switch (status) {
        case Success:
            // Issue #1041 - MPD does not seem to persist user/client made replaygain changes, so use the values read from Cantata's config.
            if (replaygainSupported() && details.applyReplayGain && !details.replayGain.isEmpty()) {
                sendCommand("replay_gain_mode "+details.replayGain.toLatin1());
            }
            serverInfo.detect();
            listPartitions();
            getStatus();
            getStats();
            getUrlHandlers();
            getTagTypes();
            getStickerSupport();
            playListInfo();
            outputs();
            determineIfaceIp();
            emit stateChanged(true);
            break;
        default:
            emit stateChanged(false);
            emit error(errorString(status), true);
            if (isInitialConnect) {
                reconnect();
            }
        }
    } else if (diffName) {
         emit stateChanged(true);
    }
    #ifdef ENABLE_HTTP_STREAM_PLAYBACK
    if (diffStreamUrl) {
        emit streamUrl(details.streamUrl);
    }
    #endif
    if (changedDir) {
        emit dirChanged();
    }
    isInitialConnect = false;
}

//void MPDConnection::disconnectMpd()
//{
//    if (State_Connected==state) {
//        disconnectFromMPD();
//        emit stateChanged(false);
//    }
//}

MPDConnection::Response MPDConnection::sendCommand(const QByteArray &command, bool emitErrors, bool retry)
{
    connTimer->stop();
    static bool reconnected=false; // If we reconnect, and send playlistinfo - dont want that call causing reconnects, and recursion!
    DBUG << (void *)(&sock) << "sendCommand:" << log(command) << emitErrors << retry;

    if (!isConnected()) {
        if ("stop"!=command) {
            emit error(tr("Failed to send command to %1 - not connected").arg(details.description()), true);
        }
        return Response(false);
    }

    if (QAbstractSocket::ConnectedState!=sock.state() && !reconnected) {
        DBUG << (void *)(&sock) << "Socket (state:" << sock.state() << ") need to reconnect";
        sock.close();
        ConnectionReturn status=connectToMPD();
        if (Success!=status) {
            disconnectFromMPD();
            emit stateChanged(false);
            emit error(errorString(status), true);
            return Response(false);
        } else {
            // Refresh playqueue...
            reconnected=true;
            listPartitions();
            playListInfo();
            getStatus();
            reconnected=false;
        }
    }

    Response response;
    if (-1==sock.write(command+'\n')) {
        DBUG << "Failed to write";
        // If we fail to write, dont wait for bytes to be written!!
        response=Response(false);
        sock.close();
    } else {
        int timeout=socketTimeout(command.length());
        DBUG << "Timeout (ms):" << timeout;
        sock.waitForBytesWritten(timeout);
        DBUG << "Socket state after write:" << (int)sock.state();
        response=readReply(sock, timeout);
    }

    if (!response.ok) {
        DBUG << log(command) << "failed";
        if (response.data.isEmpty() && retry && QAbstractSocket::ConnectedState!=sock.state() && !reconnected) {
            // Try one more time...
            // This scenario, where socket seems to be closed during/after 'write' seems to occur more often
            // when dynamizer is running. However, simply reconnecting seems to resolve the issue.
            return sendCommand(command, emitErrors, false);
        }
        clearError();
        if (emitErrors) {
            bool emitError=true;
            // Mopidy returns "incorrect arguments" for commands it does not support. The docs state that crossfade and replaygain mode
            // setting commands are not supported. So, if we get this error then just ignore it.
            if (!isMpd() && (command.startsWith("crossfade ") || command.startsWith("replay_gain_mode "))) {
                emitError=false;
            } else if (isMpd() && command.startsWith("albumart ")) {
                // MPD will report a generic "file not found" error if it can't find album art; this can happen
                // several times in a large playlist so hide this from the GUI (but report it using DBUG here).
                emitError=false;
                const auto start = command.indexOf(' ');
                const auto end = command.lastIndexOf(' ') - start;
                if (start > 0 && (end > 0 && (start + end) < command.length())) {
                    const QString filename = command.mid(start, end);
                    DBUG << "MPD reported no album art for" << filename;
                } else {
                    DBUG << "MPD albumart command was malformed:" << command;
                }
            }
            if (emitError) {
                if ((command.startsWith("add ") || command.startsWith("command_list_begin\nadd ")) && -1!=command.indexOf("\"file:///")) {
                    if (details.isLocal() && -1!=response.data.indexOf("Permission denied")) {
                        emit error(tr("Failed to load. Please check user \"mpd\" has read permission."));
                    } else if (!details.isLocal() && -1!=response.data.indexOf("Access denied")) {
                        emit error(tr("Failed to load. MPD can only play local files if connected via a local socket."));
                    } else if (!response.getError(command).isEmpty()) {
                        emit error(tr("MPD reported the following error: %1").arg(response.getError(command)));
                    } else {
                        disconnectFromMPD();
                        emit stateChanged(false);
                        emit error(tr("Failed to send command. Disconnected from %1").arg(details.description()), true);
                    }
                } else if (!response.getError(command).isEmpty()) {
                    emit error(tr("MPD reported the following error: %1").arg(response.getError(command)));
                } /*else if ("listallinfo"==command && ver>=CANTATA_MAKE_VERSION(0,18,0)) {
                    disconnectFromMPD();
                    emit stateChanged(false);
                    emit error(tr("Failed to load library. Please increase \"max_output_buffer_size\" in MPD's config file."));
                } */ else {
                    disconnectFromMPD();
                    emit stateChanged(false);
                    emit error(tr("Failed to send command. Disconnected from %1").arg(details.description()), true);
                }
            }
        }
    }
    DBUG << (void *)(&sock) << "sendCommand - sent";
    if (QAbstractSocket::ConnectedState==sock.state()) {
        connTimer->start(constConnTimer);
    } else {
        connTimer->stop();
    }
    return response;
}

/*
 * Playlist commands
 */
bool MPDConnection::isPlaylist(const QString &file)
{
    static QSet<QString> extensions = QSet<QString>() << QLatin1String("asx") << QLatin1String("cue")
                                                      << QLatin1String("m3u") << QLatin1String("m3u8")
                                                      << QLatin1String("pls") << QLatin1String("xspf");

    int pos=file.lastIndexOf('.');
    return pos>0 ? extensions.contains(file.mid(pos+1).toLower()) : false;
}

void MPDConnection::add(const QStringList &files, int action, quint8 priority, bool decreasePriority)
{
    add(files, 0, 0, action, priority, decreasePriority);
}

void MPDConnection::add(const QStringList &files, quint32 pos, quint32 size, int action, quint8 priority, bool decreasePriority)
{
    QList<quint8> prioList;
    if (priority>0) {
        prioList << priority;
    }
    add(files, pos, size, action, prioList, decreasePriority);
}

void MPDConnection::add(const QStringList &origList, quint32 pos, quint32 size, int action, const QList<quint8> &priority)
{
    add(origList, pos, size, action, priority, false);
}

void MPDConnection::add(const QStringList &origList, quint32 pos, quint32 size, int action, QList<quint8> priority, bool decreasePriority)
{
    quint32 playPos=0;
    if (0==pos && 0==size && (AddAfterCurrent==action || AppendAndPlay==action || AddAndPlay==action)) {
        Response response=sendCommand("status");
        if (response.ok) {
            MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
            if (AppendAndPlay==action) {
                playPos=sv.playlistLength;
            } else if (AddAndPlay==action) {
                pos=0;
                size=sv.playlistLength;
            } else {
                pos=sv.song+1;
                size=sv.playlistLength;
            }
        }
    }
    toggleStopAfterCurrent(false);
    if (Replace==action || ReplaceAndplay==action) {
        clear();
        getStatus();
    }

    QStringList files;
    for (const QString &file: origList) {
        if (file.startsWith(constDirPrefix)) {
            files+=getAllFiles(file.mid(constDirPrefix.length()));
        } else if (file.startsWith(constPlaylistPrefix)) {
            files+=getPlaylistFiles(file.mid(constPlaylistPrefix.length()));
        } else {
            files.append(file);
        }
    }

    QList<QStringList> fileLists;
    if (priority.count()<=1 && files.count()>constMaxFilesPerAddCommand) {
        int numChunks=(files.count()/constMaxFilesPerAddCommand)+(files.count()%constMaxFilesPerAddCommand ? 1 : 0);
        for (int i=0; i<numChunks; ++i) {
            fileLists.append(files.mid(i*constMaxFilesPerAddCommand, constMaxFilesPerAddCommand));
        }
    } else {
        fileLists.append(files);
    }

    int curSize = size;
    int curPos = pos;
    //    bool addedFile=false;
    bool havePlaylist=false;

    if (1==priority.count() && decreasePriority) {
        quint8 prio=priority.at(0);
        priority.clear();
        for (int i=0; i<files.count(); ++i) {
            priority.append(prio);
            if (prio>1) {
                prio--;
            }
        }
    }

    bool usePrio=!priority.isEmpty() && canUsePriority() && (1==priority.count() || priority.count()==files.count());
    quint8 singlePrio=usePrio && 1==priority.count() ? priority.at(0) : 0;
    QStringList cStreamFiles;
    bool sentOk=false;

    if (usePrio && Append==action && 0==curPos) {
        curPos=playQueueIds.size();
    }

    for (const QStringList &files: fileLists) {
        QByteArray send = "command_list_begin\n";

        for (int i = 0; i < files.size(); i++) {
            QString fileName=files.at(i);
            if (fileName.startsWith(QLatin1String("http://")) && fileName.contains(QLatin1String("cantata=song"))) {
                cStreamFiles.append(fileName);
            }
            if (CueFile::isCue(fileName)) {
                send += "load "+CueFile::getLoadLine(fileName)+'\n';
            } else {
                if (isPlaylist(fileName)) {
                    send+="load ";
                    havePlaylist=true;
                } else {
                    //                addedFile=true;
                    send += "add ";
                }
                send += encodeName(fileName)+'\n';
            }
            if (!havePlaylist) {
                if (0!=size) {
                    send += "move "+quote(curSize)+" "+quote(curPos)+'\n';
                }
                if (usePrio && !havePlaylist) {
                    send += "prio "+quote(singlePrio || i>=priority.count() ? singlePrio : priority.at(i))+" "+quote(curPos)+" "+quote(curPos)+'\n';
                }
            }
            curSize++;
            curPos++;
        }

        send += "command_list_end";
        sentOk=sendCommand(send).ok;
        if (!sentOk) {
            break;
        }
    }

    if (sentOk) {
        if (!cStreamFiles.isEmpty()) {
            emit cantataStreams(cStreamFiles);
        }

        if ((ReplaceAndplay==action || AddAndPlay==action) /*&& addedFile */&& !files.isEmpty()) {
            // Dont emit error if play fails, might be that playlist was not loaded...
            playFirstTrack(false);
        }

        if (AppendAndPlay==action) {
            startPlayingSong(playPos);
        }
        emit added(files);
    }
}

void MPDConnection::populate(const QStringList &files, const QList<quint8> &priority)
{
    add(files, 0, 0, Replace, priority);
}

void MPDConnection::addAndPlay(const QString &file)
{
    toggleStopAfterCurrent(false);
    Response response=sendCommand("status");
    if (response.ok) {
        MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
        QByteArray send = "command_list_begin\n";
        if (CueFile::isCue(file)) {
            send += "load "+CueFile::getLoadLine(file)+'\n';
        } else {
            send+="add "+encodeName(file)+'\n';
        }
        send+="play "+quote(sv.playlistLength)+'\n';
        send+="command_list_end";
        sendCommand(send);
    }
}

void MPDConnection::clear()
{
    toggleStopAfterCurrent(false);
    if (sendCommand("clear").ok) {
        lastUpdatePlayQueueVersion=0;
        playQueueIds.clear();
        emit cantataStreams(QList<Song>(), false);
    }
}

void MPDConnection::removeSongs(const QList<qint32> &items)
{
    toggleStopAfterCurrent(false);
    QByteArray send = "command_list_begin\n";
    for (qint32 i: items) {
        send += "deleteid "+quote(i)+'\n';
    }

    send += "command_list_end";
    sendCommand(send);
}

void MPDConnection::move(quint32 from, quint32 to)
{
    toggleStopAfterCurrent(false);
    sendCommand("move "+quote(from)+' '+quote(to));
}

void MPDConnection::move(const QList<quint32> &items, quint32 pos, quint32 size)
{
    doMoveInPlaylist(QString(), items, pos, size);
    #if 0
    QByteArray send = "command_list_begin\n";
    QList<quint32> moveItems;

    moveItems.append(items);
    std::sort(moveItems.begin(), moveItems.end());

    int posOffset = 0;

    //first move all items (starting with the biggest) to the end so we don't have to deal with changing rownums
    for (int i = moveItems.size() - 1; i >= 0; i--) {
        if (moveItems.at(i) < pos && moveItems.at(i) != size - 1) {
            // we are moving away an item that resides before the destinatino row, manipulate destination row
            posOffset++;
        }
        send += "move ";
        send += quote(moveItems.at(i));
        send += " ";
        send += quote(size - 1);
        send += '\n';
    }
    //now move all of them to the destination position
    for (int i = moveItems.size() - 1; i >= 0; i--) {
        send += "move ";
        send += quote(size - 1 - i);
        send += " ";
        send += quote(pos - posOffset);
        send += '\n';
    }

    send += "command_list_end";
    sendCommand(send);
    #endif
}

void MPDConnection::setOrder(const QList<quint32> &items)
{
    QByteArray cmd("move ");
    QByteArray send;
    QList<qint32> positions;
    quint32 numChanges=0;
    for (qint32 i=0; i<items.count(); ++i) {
        positions.append(i);
    }

    for (qint32 to=0; to<items.count(); ++to) {
        qint32 from=positions.indexOf(items.at(to));
        if (from!=to) {
            if (send.isEmpty()) {
                send = "command_list_begin\n";
            }
            send += cmd;
            send += quote(from);
            send += " ";
            send += quote(to);
            send += '\n';
            positions.move(from, to);
            numChanges++;
        }
    }

    if (!send.isEmpty()) {
        send += "command_list_end";
        // If there are more than X changes made to the playqueue, then doing partial updates
        // can hang the UI. Therefore, set lastUpdatePlayQueueVersion to 0 to cause playlistInfo
        // to be used to do a complete update.
        if (sendCommand(send).ok && numChanges>constMaxPqChanges) {
            lastUpdatePlayQueueVersion=0;
        }
    }
}

void MPDConnection::shuffle()
{
    toggleStopAfterCurrent(false);
    sendCommand("shuffle");
}

void MPDConnection::shuffle(quint32 from, quint32 to)
{
    toggleStopAfterCurrent(false);
    sendCommand("shuffle "+quote(from)+':'+quote(to+1));
}

void MPDConnection::currentSong()
{
    Response response=sendCommand("currentsong");
    if (response.ok) {
        emit currentSongUpdated(MPDParseUtils::parseSong(response.data, MPDParseUtils::Loc_PlayQueue));
    }
}

/*
 * Call "plchangesposid" to recieve a list of positions+ids that have been changed since the last update.
 * If we have ids in this list that we don't know about, then these are new songs - so we call
 * "playlistinfo <pos>" to get the song information.
 *
 * Any songs that are know about, will actually be sent with empty data - as the playqueue model will
 * already hold these songs.
 */
void MPDConnection::playListChanges()
{
    DBUG << "playListChanges" << lastUpdatePlayQueueVersion << playQueueIds.size();
    if (0==lastUpdatePlayQueueVersion || 0==playQueueIds.size()) {
        playListInfo();
        return;
    }

    QByteArray data = "plchangesposid "+quote(lastUpdatePlayQueueVersion);
    Response status=sendCommand("status"); // We need an updated status so as to detect deletes at end of list...
    Response response=sendCommand(data, false);
    if (response.ok && status.ok && isPlayQueueIdValid()) {
        MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
        if (lastUpdatePlayQueueVersion==sv.playlist) {
            return; // Playlist is already up-to-date
        }
        lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=sv.playlist;
        emitStatusUpdated(sv);
        QList<MPDParseUtils::IdPos> changes=MPDParseUtils::parseChanges(response.data);
        if (!changes.isEmpty()) {
            if (changes.count()>constMaxPqChanges) {
                playListInfo();
                return;
            }
            bool first=true;
            quint32 firstPos=0;
            QList<Song> songs;
            QList<Song> newCantataStreams;
            QList<qint32> ids;
            QSet<qint32> prevIds=Utils::listToSet(playQueueIds);
            QSet<qint32> strmIds;

            for (const MPDParseUtils::IdPos &idp: changes) {
                if (first) {
                    first=false;
                    firstPos=idp.pos;
                    if (idp.pos!=0) {
                        for (quint32 i=0; i<idp.pos; ++i) {
                            Song s;
                            if (i>=(unsigned int)playQueueIds.count()) { // Just for safety...
                                playListInfo();
                                return;
                            }
                            s.id=playQueueIds.at(i);
                            songs.append(s);
                            ids.append(s.id);
                            if (streamIds.contains(s.id)) {
                                strmIds.insert(s.id);
                            }
                        }
                    }
                }

                if (prevIds.contains(idp.id) && !streamIds.contains(idp.id)) {
                    Song s;
                    s.id=idp.id;
//                     s.pos=idp.pos;
                    songs.append(s);
                } else {
                    // New song!
                    data = "playlistinfo ";
                    data += quote(idp.pos);
                    response=sendCommand(data);
                    if (!response.ok) {
                        playListInfo();
                        return;
                    }
                    Song s=MPDParseUtils::parseSong(response.data, MPDParseUtils::Loc_PlayQueue);
                    s.id=idp.id;
//                     s.pos=idp.pos;
                    songs.append(s);
                    if (s.isCdda()) {
                        newCantataStreams.append(s);
                    } else if (s.isStream()) {
                        if (s.isCantataStream()) {
                            newCantataStreams.append(s);
                        } else {
                            strmIds.insert(s.id);
                        }
                    }
                }
                ids.append(idp.id);
            }

            // Dont think this section is ever called, but leave here to be safe!!!
            // For some reason if we have 10 songs in our playlist and we move pos 2 to pos 1, MPD sends all ids from pos 1 onwards
            if (firstPos+changes.size()<=sv.playlistLength && (sv.playlistLength<=(unsigned int)playQueueIds.length())) {
                for (quint32 i=firstPos+changes.size(); i<sv.playlistLength; ++i) {
                    Song s;
                    s.id=playQueueIds.at(i);
                    songs.append(s);
                    ids.append(s.id);
                    if (streamIds.contains(s.id)) {
                        strmIds.insert(s.id);
                    }
                }
            }

            playQueueIds=ids;
            streamIds=strmIds;
            if (!newCantataStreams.isEmpty()) {
                emit cantataStreams(newCantataStreams, true);
            }
            QSet<qint32> removed=prevIds-Utils::listToSet(playQueueIds);
            if (!removed.isEmpty()) {
                emit removedIds(removed);
            }
            emit playlistUpdated(songs, false);
            if (songs.isEmpty()) {
                stopVolumeFade();
            }
            return;
        }
    }

    playListInfo();
}

void MPDConnection::playListInfo()
{
    Response response=sendCommand("playlistinfo");
    QList<Song> songs;
    if (response.ok) {
        lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion;
        songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_PlayQueue);
        playQueueIds.clear();
        streamIds.clear();

        QList<Song> cStreams;
        for (const Song &s: songs) {
            playQueueIds.append(s.id);
            if (s.isCdda()) {
                cStreams.append(s);
            } else if (s.isStream()) {
                if (s.isCantataStream()) {
                    cStreams.append(s);
                } else {
                    streamIds.insert(s.id);
                }
            }
        }
        emit cantataStreams(cStreams, false);
        if (songs.isEmpty()) {
            stopVolumeFade();
        }
        Response status=sendCommand("status");
        if (status.ok) {
            MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
            lastUpdatePlayQueueVersion=lastStatusPlayQueueVersion=sv.playlist;
            emitStatusUpdated(sv);
        }
    }
    emit playlistUpdated(songs, true);
}

/*
 * Playback commands
 */
void MPDConnection::setCrossFade(int secs)
{
    sendCommand("crossfade "+quote(secs));
}

void MPDConnection::setReplayGain(const QString &v)
{
    if (replaygainSupported()) {
        // Issue #1041 - MPD does not seem to persist user/client made replaygain changes, so store in Cantata's config file.
        Settings::self()->saveReplayGain(details.name, v);
        sendCommand("replay_gain_mode "+v.toLatin1());
    }
}

void MPDConnection::getReplayGain()
{
    if (replaygainSupported()) {
        QStringList lines=QString(sendCommand("replay_gain_status").data).split('\n', CANTATA_SKIP_EMPTY);

        if (2==lines.count() && "OK"==lines[1] && lines[0].startsWith(QLatin1String("replay_gain_mode: "))) {
            QString mode=lines[0].mid(18);
            // Issue #1041 - MPD does not seem to persist user/client made replaygain changes, so store in Cantata's config file.
            Settings::self()->saveReplayGain(details.name, mode);
            emit replayGain(mode);
        } else {
            emit replayGain(QString());
        }
    }
}

void MPDConnection::goToNext()
{
    toggleStopAfterCurrent(false);
    Response status=sendCommand("status");
    if (status.ok) {
        MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
        if (MPDState_Stopped!=sv.state && -1!=sv.nextSongId) {
            stopVolumeFade();
            sendCommand("next");
        }
    }
}

static inline QByteArray value(bool b)
{
    return MPDConnection::quote(b ? 1 : 0);
}

void MPDConnection::setPause(bool toggle)
{
    toggleStopAfterCurrent(false);
    stopVolumeFade();
    sendCommand("pause "+value(toggle));
}

void MPDConnection::play()
{
    playFirstTrack(true);
}

void MPDConnection::playFirstTrack(bool emitErrors)
{
    toggleStopAfterCurrent(false);
    stopVolumeFade();
    sendCommand("play 0", emitErrors);
}

void MPDConnection::seek(qint32 offset)
{
    if (0==offset) {
        QObject *s=sender();
        offset=s ? s->property("offset").toInt() : 0;
        if (0==offset) {
            return;
        }
    }
    toggleStopAfterCurrent(false);
    Response response=sendCommand("status");
    if (response.ok) {
        MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
        if (-1==sv.songId) {
            return;
        }
        if (offset>0) {
            if (sv.timeElapsed+offset<sv.timeTotal) {
                setSeek(sv.song, sv.timeElapsed+offset);
            } else {
                goToNext();
            }
        } else {
            if (sv.timeElapsed+offset>=0) {
                setSeek(sv.song, sv.timeElapsed+offset);
            } else {
                // Not sure about this!!!
                /*goToPrevious();*/
                setSeek(sv.song, 0);
            }
        }
    }
}

void MPDConnection::startPlayingSong(quint32 song)
{
    toggleStopAfterCurrent(false);
    sendCommand("play "+quote(song));
}

void MPDConnection::startPlayingSongId(qint32 songId)
{
    toggleStopAfterCurrent(false);
    stopVolumeFade();
    sendCommand("playid "+quote(songId));
}

void MPDConnection::goToPrevious()
{
    toggleStopAfterCurrent(false);
    stopVolumeFade();
    Response status=sendCommand("status");
    if (status.ok) {
        MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
        if (sv.timeElapsed>4) {
            setSeekId(sv.songId, 0);
            return;
        }
    }
    sendCommand("previous");
}

void MPDConnection::setConsume(bool toggle)
{
    sendCommand("consume "+value(toggle));
}

void MPDConnection::setRandom(bool toggle)
{
    sendCommand("random "+value(toggle));
}

void MPDConnection::setRepeat(bool toggle)
{
    sendCommand("repeat "+value(toggle));
}

void MPDConnection::setSingle(bool toggle)
{
    sendCommand("single "+value(toggle));
}

void MPDConnection::setSeek(quint32 song, quint32 time)
{
    sendCommand("seek "+quote(song)+' '+quote(time));
}

void MPDConnection::setSeekId(qint32 songId, quint32 time)
{
    if (-1==songId) {
        songId=currentSongId;
    }
    if (-1==songId) {
        return;
    }
    if (songId!=currentSongId || 0==time) {
        toggleStopAfterCurrent(false);
    }
    if (sendCommand("seekid "+quote(songId)+' '+quote(time)).ok) {
        if (stopAfterCurrent && songId==currentSongId && songPos>time) {
            songPos=time;
        }
    }
}

void MPDConnection::setVolume(int vol) //Range accepted by MPD: 0-100
{
    if (-1==vol) {
        if (restoreVolume>=0) {
            sendCommand("setvol "+quote(restoreVolume), false);
        }
        if (volumeFade) {
            sendCommand("stop");
        }
        restoreVolume=-1;
    } else if (vol>=0) {
        unmuteVol=-1;
        sendCommand("setvol "+quote(vol), false);
    }
}

void MPDConnection::toggleMute()
{
    if (unmuteVol>0) {
        sendCommand("setvol "+quote(unmuteVol), false);
        unmuteVol=-1;
    } else {
        Response status=sendCommand("status");
        if (status.ok) {
            MPDStatusValues sv=MPDParseUtils::parseStatus(status.data);
            if (sv.volume>0) {
                unmuteVol=sv.volume;
                sendCommand("setvol "+quote(0), false);
            }
        }
    }
}

void MPDConnection::stopPlaying(bool afterCurrent)
{
    toggleStopAfterCurrent(afterCurrent);
    if (!stopAfterCurrent) {
        if (!startVolumeFade()) {
            sendCommand("stop");
        }
    }
}

void MPDConnection::clearStopAfter()
{
    toggleStopAfterCurrent(false);
}

void MPDConnection::getStats()
{
    Response response=sendCommand("stats");
    if (response.ok) {
        MPDStatsValues stats=MPDParseUtils::parseStats(response.data);
        dbUpdate=stats.dbUpdate;
        if (isMopidy()) {
            // Set version to 1 so that SQL cache is updated - it uses 0 as intial value
            dbUpdate=stats.dbUpdate=1;
        }
        emit statsUpdated(stats);
    }
}

void MPDConnection::getStatus()
{
   Response response=sendCommand("status");
    if (response.ok) {
        MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
        lastStatusPlayQueueVersion=sv.playlist;
        if (details.partition != sv.partition) {
            details.partition = sv.partition;
            Settings::self()->saveConnectionDetails(details);
            lastUpdatePlayQueueVersion=0;
            playQueueIds.clear();
        }
        if (currentSongId!=sv.songId) {
            stopVolumeFade();
        }
        if (stopAfterCurrent && (currentSongId!=sv.songId || (songPos>0 && sv.timeElapsed<(qint32)songPos))) {
            stopVolumeFade();
            if (sendCommand("stop").ok) {
                sv.state=MPDState_Stopped;
            }
            toggleStopAfterCurrent(false);
        }
        currentSongId=sv.songId;
        if (!isUpdatingDb && -1!=sv.updatingDb) {
            isUpdatingDb=true;
            emit updatingDatabase();
        } else if (isUpdatingDb && -1==sv.updatingDb) {
            isUpdatingDb=false;
            emit updatedDatabase();
        }
        emitStatusUpdated(sv);

        // If playlist length does not match number of IDs, then refresh
        if (sv.playlistLength!=static_cast<size_t>(playQueueIds.length())) {
            playListInfo();
        }
    }
}

void MPDConnection::getUrlHandlers()
{
    Response response=sendCommand("urlhandlers");
    if (response.ok) {
        handlers=Utils::listToSet(MPDParseUtils::parseList(response.data, QByteArray("handler: ")));
        DBUG << handlers;
    }
}

void MPDConnection::getTagTypes()
{
    Response response=sendCommand("tagtypes");
    if (response.ok) {
        tagTypes=Utils::listToSet(MPDParseUtils::parseList(response.data, QByteArray("tagtype: ")));
    }
}

void MPDConnection::getCover(const Song &song)
{
    int dataToRead = -1;
    int imageSize = 0;
    QByteArray imageData;
    bool firstRun = true;
    QString path=Utils::getDir(song.file);
    while (dataToRead != 0) {
        Response response=sendCommand("albumart "+encodeName(path)+" "+QByteArray::number(firstRun ? 0 : (imageSize - dataToRead)));
        if (!response.ok) {
            DBUG << "albumart query failed";
            break;
        }

        static const QByteArray constSize("size: ");
        static const QByteArray constBinary("binary: ");

        auto sizeStart = strstr(response.data.constData(), constSize.constData());
        if (!sizeStart) {
            DBUG << "Failed to get size start";
            break;
        }
        auto sizeEnd = strchr(sizeStart, '\n');
        if (!sizeEnd) {
            DBUG << "Failed to get size end";
            break;
        }

        auto chunkSizeStart = strstr(sizeEnd, constBinary.constData());
        if (!chunkSizeStart) {
            DBUG << "Failed to get chunk size start";
            break;
        }
        auto chunkSizeEnd = strchr(chunkSizeStart, '\n');
        if (!chunkSizeEnd) {
            DBUG << "Failed to chunk size end";
            break;
        }

        if (firstRun) {
            imageSize = QByteArray(sizeStart+constSize.length(), sizeEnd-(sizeStart+constSize.length())).toUInt();
            imageData.reserve(imageSize);
            dataToRead = imageSize;
            firstRun = false;
            DBUG << "image size" << imageSize;
        }

        int chunkSize = QByteArray(chunkSizeStart+constBinary.length(), chunkSizeEnd-(chunkSizeStart+constBinary.length())).toUInt();
        DBUG << "chunk size" << chunkSize;

        int startOfChunk=(chunkSizeEnd+1)-response.data.constData();
        if (startOfChunk+chunkSize > response.data.length()) {
            DBUG << "Invalid chunk size";
            break;
        }

        imageData.append(chunkSizeEnd+1, chunkSize);
        dataToRead -= chunkSize;
    }

    DBUG << dataToRead << imageData.size();
    emit albumArt(song, 0==dataToRead ? imageData : QByteArray());
}

/*
 * Data is written during idle.
 * Retrieve it and parse it
 */
void MPDConnection::idleDataReady()
{
    DBUG << "idleDataReady";
    if (0==idleSocket.bytesAvailable()) {
        return;
    }
    parseIdleReturn(readFromSocket(idleSocket));
}

/*
 * Socket state has changed, currently we only use this to gracefully
 * handle disconnects.
 */
void MPDConnection::onSocketStateChanged(QAbstractSocket::SocketState socketState)
{
    if (socketState == QAbstractSocket::ClosingState){
        bool wasConnected=State_Connected==state;
        disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
        DBUG << "onSocketStateChanged";
        if (QAbstractSocket::ConnectedState==idleSocket.state()) {
            idleSocket.disconnectFromHost();
        }
        idleSocket.close();
        ConnectionReturn status=Success;
        if (wasConnected && Success!=(status=connectToMPD(idleSocket, true))) {
            // Failed to connect idle socket - so close *both*
            disconnectFromMPD();
            emit stateChanged(false);
            emit error(errorString(status), true);
        }
        if (QAbstractSocket::ConnectedState==idleSocket.state()) {
            connect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)), Qt::QueuedConnection);
        }
    }
}

/*
 * Parse data returned by the mpd deamon on an idle commond.
 */
void MPDConnection::parseIdleReturn(const QByteArray &data)
{
    DBUG << "parseIdleReturn:" << data;

    Response response(data.endsWith(constOkNlValue), data);
    if (!response.ok) {
        DBUG << "idle failed? reconnect";
        disconnect(&idleSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(onSocketStateChanged(QAbstractSocket::SocketState)));
        if (QAbstractSocket::ConnectedState==idleSocket.state()) {
            idleSocket.disconnectFromHost();
        }
        idleSocket.close();
        ConnectionReturn status=connectToMPD(idleSocket, true);
        if (Success!=status) {
            // Failed to connect idle socket - so close *both*
            disconnectFromMPD();
            emit stateChanged(false);
            emit error(errorString(status), true);
        }
        return;
    }

    QList<QByteArray> lines = data.split('\n');

    /*
     * See http://www.musicpd.org/doc/protocol/ch02.html
     */
    bool playListUpdated=false;
    bool statusUpdated=false;
    for (const QByteArray &line: lines) {
        if (line.startsWith(constIdleChangedKey)) {
            QByteArray value=line.mid(constIdleChangedKey.length());
            if (constIdleDbValue==value) {
                getStats();
                getStatus();
                playListInfo();
                playListUpdated=true;
            } else if (constIdleUpdateValue==value) {
                getStats();
                getStatus();
            } else if (constIdleStoredPlaylistValue==value) {
                listPlaylists();
                listStreams();
            } else if (constIdlePlaylistValue==value) {
                if (!playListUpdated) {
                    playListChanges();
                }
            } else if (!statusUpdated && (constIdlePlayerValue==value || constIdleMixerValue==value || constIdleOptionsValue==value)) {
                getStatus();
                getReplayGain();
                statusUpdated=true;
            } else if (constIdlePartitionValue==value) {
                listPartitions();
            } else if (constIdleOutputValue==value) {
                outputs();
            } else if (constIdleStickerValue==value) {
                emit stickerDbChanged();
            } else if (constIdleSubscriptionValue==value) {
                //if (dynamicId.isEmpty()) {
                    setupRemoteDynamic();
                //}
            } else if (constIdleMessageValue==value) {
                readRemoteDynamicMessages();
            }
        }
    }

    while (!idleSocketCommandQueue.isEmpty()) {
        idleSocket.write(idleSocketCommandQueue.dequeue()+'\n');
        idleSocket.waitForBytesWritten();
        readReply(idleSocket);
    }

    DBUG << (void *)(&idleSocket) << "write idle";
    idleSocket.write("idle\n");
    idleSocket.waitForBytesWritten();
}

void MPDConnection::listPartitions()
{
    Response response=sendCommand("listpartitions", false);
    if (response.ok) {
        emit partitionsUpdated(MPDParseUtils::parsePartitions(response.data));
    } else {
        // Send an empty list to indicate lack of partition support for this MPD server.
        emit partitionsUpdated({});
    }
}

void MPDConnection::changePartition(QString name)
{
    stopVolumeFade();
    toggleStopAfterCurrent(false);

    QByteArray cmd = "partition " + encodeName(name);
    if (sendCommand(cmd).ok) {
        getStatus();
        idleSocketCommandQueue.enqueue(cmd);
        idleSocket.write("noidle\n");
        idleSocket.waitForBytesWritten();
    }
}

void MPDConnection::newPartition(QString name)
{
    QByteArray cmd = "newpartition " + encodeName(name);
    if (sendCommand(cmd).ok) {
        listPartitions();
    }
}

void MPDConnection::delPartition(QString name)
{
    QByteArray cmd = "delpartition " + encodeName(name);
    if (sendCommand(cmd).ok) {
        listPartitions();
    }
}

void MPDConnection::outputs()
{
    Response response=sendCommand("outputs");
    if (response.ok) {
        QList<Output> outputs = MPDParseUtils::parseOuputs(response.data);

        // We need to temporarily switch to the default partition in order
        // to collect the details of all available outputs.
        if (!details.partition.isEmpty() && details.partition != "default") {
            QByteArray returnCmd = "partition " + encodeName(details.partition);
            Response defaultResponse=sendCommand("command_list_begin\npartition default\noutputs\n" + returnCmd + "\ncommand_list_end");
            if (defaultResponse.ok) {
                QSet<QString> existingNames;
                for (const Output &o: outputs) {
                    existingNames << o.name;
                }
                QList<Output> defaultOutputs = MPDParseUtils::parseOuputs(defaultResponse.data);
                for (Output &o: defaultOutputs) {
                    if (!existingNames.contains(o.name)) {
                        o.inCurrentPartition = false;
                        outputs << o;
                    }
                }
            } else {
                sendCommand(returnCmd);
            }
        }

        emit outputsUpdated(outputs);
    }
}

void MPDConnection::enableOutput(quint32 id, bool enable)
{
    if (sendCommand((enable ? "enableoutput " : "disableoutput ")+quote(id)).ok) {
        outputs();
    }
}

void MPDConnection::moveOutput(QString name)
{
    if (sendCommand("moveoutput " + encodeName(name)).ok) {
        outputs();
    }
}

/*
 * Admin commands
 */
void MPDConnection::updateMaybe()
{
    if (!details.autoUpdate) {
        update();
    }
}

void MPDConnection::update()
{
    if (isMopidy()) {
        // Mopidy does not support MPD's update command. So, when user presses update DB, what we
        // just reload the library.
        loadLibrary();
    } else {
        sendCommand("update");
    }
}

/*
 * Database commands
 */
void MPDConnection::loadLibrary()
{
    DBUG << "loadLibrary";
    isListingMusic=true;
    emit updatingLibrary(dbUpdate);
    QList<Song> songs;
    recursivelyListDir("/", songs);
    emit updatedLibrary();
    isListingMusic=false;
}

void MPDConnection::listFolder(const QString &folder)
{
    DBUG << "listFolder" << folder;
    bool topLevel="/"==folder || ""==folder;
    Response response=sendCommand(topLevel ? "lsinfo" : ("lsinfo "+encodeName(folder)));
    QStringList subFolders;
    QList<Song> songs;
    if (response.ok) {
        MPDParseUtils::parseDirItems(response.data, QString(), ver, songs, folder, subFolders, MPDParseUtils::Loc_Browse);
    }
    emit folderContents(folder, subFolders, songs);
}

/*
 * Playlists commands
 */
// void MPDConnection::listPlaylist(const QString &name)
// {
//     QByteArray data = "listplaylist ";
//     data += encodeName(name);
//     sendCommand(data);
// }

void MPDConnection::listPlaylists()
{
    // Don't report errors here. If user has disabled playlists, then MPD will report an error
    // Issues #1090 #1284
    Response response=sendCommand("listplaylists", false);
    if (response.ok) {
        QList<Playlist> playlists=MPDParseUtils::parsePlaylists(response.data);
        playlists.removeAll((Playlist(constStreamsPlayListName)));
        emit playlistsRetrieved(playlists);
    }
}

void MPDConnection::playlistInfo(const QString &name)
{
    Response response=sendCommand("listplaylistinfo "+encodeName(name));
    if (response.ok) {
        emit playlistInfoRetrieved(name, MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Playlists));
    }
}

void MPDConnection::loadPlaylist(const QString &name, bool replace)
{
    if (replace) {
        clear();
        getStatus();
    }

    if (sendCommand("load "+encodeName(name)).ok) {
        if (replace) {
            playFirstTrack(false);
        }
        emit playlistLoaded(name);
    }
}

void MPDConnection::renamePlaylist(const QString oldName, const QString newName)
{
    if (sendCommand("rename "+encodeName(oldName)+' '+encodeName(newName), false).ok) {
        emit playlistRenamed(oldName, newName);
    } else {
        emit error(tr("Failed to rename <b>%1</b> to <b>%2</b>").arg(oldName).arg(newName));
    }
}

void MPDConnection::removePlaylist(const QString &name)
{
    sendCommand("rm "+encodeName(name));
}

void MPDConnection::savePlaylist(const QString &name, bool overwrite)
{
    if (overwrite) {
        sendCommand("rm "+encodeName(name), false);
    }
    if (!sendCommand("save "+encodeName(name), false).ok) {
        emit error(tr("Failed to save <b>%1</b>").arg(name));
    }
}

void MPDConnection::addToPlaylist(const QString &name, const QStringList &songs, quint32 pos, quint32 size)
{
    if (songs.isEmpty()) {
        return;
    }

    if (!name.isEmpty()) {
        for (const QString &song: songs) {
            if (CueFile::isCue(song)) {
                emit error(tr("You cannot add parts of a cue sheet to a playlist!")+QLatin1String(" (")+song+QLatin1Char(')'));
                return;
            } else if (isPlaylist(song)) {
                emit error(tr("You cannot add a playlist to another playlist!")+QLatin1String(" (")+song+QLatin1Char(')'));
                return;
            }
        }
    }

    QStringList files;
    for (const QString &file: songs) {
        if (file.startsWith(constDirPrefix)) {
            files+=getAllFiles(file.mid(constDirPrefix.length()));
        } else if (file.startsWith(constPlaylistPrefix)) {
            files+=getPlaylistFiles(file.mid(constPlaylistPrefix.length()));
        } else {
            files.append(file);
        }
    }

    QByteArray encodedName=encodeName(name);
    QByteArray send = "command_list_begin\n";
    for (const QString &s: files) {
        send += "playlistadd "+encodedName+" "+encodeName(s)+'\n';
    }
    send += "command_list_end";

    if (sendCommand(send).ok) {
        if (size>0) {
            QList<quint32> items;
            for(int i=0; i<songs.count(); ++i) {
                items.append(size+i);
            }
            doMoveInPlaylist(name, items, pos, size+songs.count());
        }
    } else {
        playlistInfo(name);
    }
}

void MPDConnection::removeFromPlaylist(const QString &name, const QList<quint32> &positions)
{
    if (positions.isEmpty()) {
        return;
    }

    QByteArray encodedName=encodeName(name);
    QList<quint32> sorted=positions;
    QList<quint32> removed;

    std::sort(sorted.begin(), sorted.end());

    for (int i=sorted.count()-1; i>=0; --i) {
        quint32 idx=sorted.at(i);
        QByteArray data = "playlistdelete ";
        data += encodedName;
        data += " ";
        data += quote(idx);
        if (sendCommand(data).ok) {
            removed.prepend(idx);
        } else {
            break;
        }
    }

    if (removed.count()) {
        emit removedFromPlaylist(name, removed);
    }
//     playlistInfo(name);
}

void MPDConnection::setPriority(const QList<qint32> &ids, quint8 priority, bool decreasePriority)
{
    if (canUsePriority()) {
        QMap<qint32, quint8> tracks;
        QByteArray send = "command_list_begin\n";

        for (quint32 id: ids) {
            tracks.insert(id, priority);
            send += "prioid "+quote(priority)+" "+quote(id)+'\n';
            if (decreasePriority && priority>0) {
                priority--;
            }
        }

        send += "command_list_end";
        if (sendCommand(send).ok) {
            emit prioritySet(tracks);
        }
    }
}

void MPDConnection::search(const QString &field, const QString &value, int id)
{
    QList<Song> songs;
    QByteArray cmd;

    if (field==constModifiedSince) {
        time_t v=0;
        if (QRegExp("\\d*").exactMatch(value)) {
            #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
            v=QDateTime::currentDateTime().date().startOfDay().toTime_t()-(value.toInt()*24*60*60);
            #else
            v=QDateTime(QDateTime::currentDateTime().date()).toTime_t()-(value.toInt()*24*60*60);
            #endif
        } else if (QRegExp("^((19|20)\\d\\d)[-/](0[1-9]|1[012])[-/](0[1-9]|[12][0-9]|3[01])$").exactMatch(value)) {
            QDateTime dt=QDateTime::fromString(QString(value).replace("/", "-"), Qt::ISODate);
            if (dt.isValid()) {
                v=dt.toTime_t();
            }
        }
        if (v>0) {
            cmd="find "+field.toLatin1()+" "+quote(v);
        }
    } else {
        cmd="search "+field.toLatin1()+" "+encodeName(value);
    }

    if (!cmd.isEmpty()) {
        Response response=sendCommand(cmd);
        if (response.ok) {
            songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Search);

            if (QLatin1String("any")==field) {
                // When searching on 'any' MPD ignores filename/paths! So, do another
                // search on these, and combine results.
                response=sendCommand("search file "+encodeName(value));
                if (response.ok) {
                    QList<Song> otherSongs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Search);
                    if (!otherSongs.isEmpty()) {
                        QSet<QString> fileNames;
                        for (const auto &s: songs) {
                            fileNames.insert(s.file);
                        }
                        for (const auto &s: otherSongs) {
                            if (!fileNames.contains(s.file)) {
                                songs.append(s);
                            }
                        }
                    }
                }
            }
            std::sort(songs.begin(), songs.end());
        }
    }
    emit searchResponse(id, songs);
}

void MPDConnection::search(const QByteArray &query, const QString &id)
{
    QList<Song> songs;
    if (query.isEmpty()) {
        Response response=sendCommand("list albumartist", false, false);
        if (response.ok) {
            QList<QByteArray> lines = response.data.split('\n');
            for (const QByteArray &line: lines) {
                if (line.startsWith("AlbumArtist: ")) {
                    Response resp = sendCommand("find albumartist " + encodeName(QString::fromUtf8(line.mid(13))) , false, false);
                    if (resp.ok) {
                        songs += MPDParseUtils::parseSongs(resp.data, MPDParseUtils::Loc_Search);
                    }
                }
            }
        }
    } else if (query.startsWith("RATING:")) {
        QList<QByteArray> parts = query.split(':');
        if (3==parts.length()) {
            Response response=sendCommand("sticker find song \"\" rating", false, false);
            if (response.ok) {
                int min = parts.at(1).toInt();
                int max = parts.at(2).toInt();
                QList<MPDParseUtils::Sticker> stickers=MPDParseUtils::parseStickers(response.data, constRatingSticker);
                if (!stickers.isEmpty()) {
                    for (const MPDParseUtils::Sticker &sticker: stickers) {
                        if (!sticker.file.isEmpty() && !sticker.value.isEmpty()) {
                            int val = sticker.value.toInt();
                            if (val>=min && val<=max) {
                                Response resp = sendCommand("find file " + encodeName(QString::fromUtf8(sticker.file)) , false, false);
                                if (resp.ok) {
                                    songs += MPDParseUtils::parseSong(resp.data, MPDParseUtils::Loc_Search);
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        Response response=sendCommand(query);
        if (response.ok) {
            songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Search);
        }
    }
    emit searchResponse(id, songs);
}

void MPDConnection::listStreams()
{
    Response response=sendCommand("listplaylistinfo "+encodeName(constStreamsPlayListName), false);
    QList<Stream> streams;
    if (response.ok) {
        QList<Song> songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Streams);
        for (const Song &song: songs) {
            streams.append(Stream(song.file, song.name()));
        }
    }
    clearError();
    emit streamList(streams);
}

void MPDConnection::saveStream(const QString &url, const QString &name)
{
    if (sendCommand("playlistadd "+encodeName(constStreamsPlayListName)+" "+encodeName(MPDParseUtils::addStreamName(url, name, true))).ok) {
        emit savedStream(url, name);
    }
}

void MPDConnection::removeStreams(const QList<quint32> &positions)
{
    if (positions.isEmpty()) {
        return;
    }

    QByteArray encodedName=encodeName(constStreamsPlayListName);
    QList<quint32> sorted=positions;
    QList<quint32> removed;

    std::sort(sorted.begin(), sorted.end());

    for (int i=sorted.count()-1; i>=0; --i) {
        quint32 idx=sorted.at(i);
        QByteArray data = "playlistdelete ";
        data += encodedName;
        data += " ";
        data += quote(idx);
        if (sendCommand(data).ok) {
            removed.prepend(idx);
        } else {
            break;
        }
    }

    emit removedStreams(removed);
}

void MPDConnection::editStream(const QString &url, const QString &name, quint32 position)
{
    QByteArray encodedName=encodeName(constStreamsPlayListName);
    if (sendCommand("playlistdelete "+encodedName+" "+quote(position)).ok &&
        sendCommand("playlistadd "+encodedName+" "+encodeName(MPDParseUtils::addStreamName(url, name))).ok) {
//        emit editedStream(url, name, position);
        listStreams();
    }
}

void MPDConnection::sendClientMessage(const QString &channel, const QString &msg, const QString &clientName)
{
    if (!sendCommand("sendmessage "+channel.toUtf8()+" "+msg.toUtf8(), false).ok) {
        emit error(tr("Failed to send '%1' to %2. Please check %2 is registered with MPD.").arg(msg).arg(clientName.isEmpty() ? channel : clientName));
        emit clientMessageFailed(channel, msg);
    }
}

void MPDConnection::sendDynamicMessage(const QStringList &msg)
{
    // Check whether cantata-dynamic is still alive, by seeing if its channel is still open...
    if (1==msg.count() && QLatin1String("ping")==msg.at(0)) {
        Response response=sendCommand("channels");
        if (!response.ok || !(Utils::listToSet(MPDParseUtils::parseList(response.data, QByteArray("channel: ")))).contains(constDynamicIn)) {
            emit dynamicSupport(false);
        }
        return;
    }

    QByteArray data;
    for (QString part: msg) {
        if (data.isEmpty()) {
            data+='\"'+part.toUtf8()+':'+dynamicId;
        } else {
            part=part.replace('\"', "{q}");
            part=part.replace("{", "{ob}");
            part=part.replace("}", "{cb}");
            part=part.replace("\n", "{n}");
            part=part.replace(":", "{c}");
            data+=':'+part.toUtf8();
        }
    }

    data+='\"';
    if (!sendCommand("sendmessage "+constDynamicIn+" "+data).ok) {
        emit dynamicSupport(false);
    }
}

void MPDConnection::moveInPlaylist(const QString &name, const QList<quint32> &items, quint32 pos, quint32 size)
{
    if (doMoveInPlaylist(name, items, pos, size)) {
        emit movedInPlaylist(name, items, pos);
    }
//     playlistInfo(name);
}

bool MPDConnection::doMoveInPlaylist(const QString &name, const QList<quint32> &items, quint32 pos, quint32 size)
{
    if (name.isEmpty()) {
        toggleStopAfterCurrent(false);
    }
    QByteArray cmd = name.isEmpty() ? "move " : ("playlistmove "+encodeName(name)+" ");
    QByteArray send = "command_list_begin\n";
    QList<quint32> moveItems=items;
    int posOffset = 0;

    std::sort(moveItems.begin(), moveItems.end());

    //first move all items (starting with the biggest) to the end so we don't have to deal with changing rownums
    for (int i = moveItems.size() - 1; i >= 0; i--) {
        if (moveItems.at(i) < pos && moveItems.at(i) != size - 1) {
            // we are moving away an item that resides before the destination row, manipulate destination row
            posOffset++;
        }
        send += cmd;
        send += quote(moveItems.at(i));
        send += " ";
        send += quote(size - 1);
        send += '\n';
    }
    //now move all of them to the destination position
    for (int i = moveItems.size() - 1; i >= 0; i--) {
        send += cmd;
        send += quote(size - 1 - i);
        send += " ";
        send += quote(pos - posOffset);
        send += '\n';
    }

    send += "command_list_end";
    return sendCommand(send).ok;
}

void MPDConnection::toggleStopAfterCurrent(bool afterCurrent)
{
    if (afterCurrent!=stopAfterCurrent) {
        stopAfterCurrent=afterCurrent;
        songPos=0;
        if (stopAfterCurrent && 1==playQueueIds.count()) {
            Response response=sendCommand("status");
            if (response.ok) {
                MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
                songPos=sv.timeElapsed;
            }
        }
        emit stopAfterCurrentChanged(stopAfterCurrent);
    }
}

bool MPDConnection::recursivelyListDir(const QString &dir, QList<Song> &songs)
{
    bool topLevel="/"==dir || ""==dir;

    if (topLevel && isMpd()) {
        // UPnP database backend does not list separate metadata items, so if "list genre" returns
        // empty response assume this is a UPnP backend and dont attempt to get rest of data...
        // Although we dont use "list XXX", lsinfo will return duplciate items (due to the way most
        // UPnP servers returing directories of classifications - Genre/Album/Tracks, Artist/Album/Tracks,
        // etc...
        Response response=sendCommand("list genre", false, false);
        if (!response.ok || response.data.split('\n').length()<3) { // 2 lines - OK and blank
            // ..just to be 100% sure, check no artists either...
            response=sendCommand("list artist", false, false);
            if (!response.ok || response.data.split('\n').length()<3) { // 2 lines - OK and blank
                return false;
            }
        }
    }

    Response response=sendCommand(topLevel
                                    ? serverInfo.getTopLevelLsinfo()
                                    : ("lsinfo "+encodeName(dir)));
    if (response.ok) {
        QStringList subDirs;
        QList<Song> dirSongs;
        MPDParseUtils::parseDirItems(response.data, details.dir, ver, dirSongs, dir, subDirs, MPDParseUtils::Loc_Library);
        // If we have only 1 sug dir and its ".cue" then this is (probably) MPD's trat CUE as a directory
        // therefore we ignore any files in this directory as they will be the source files of the CUE
        if (1!=subDirs.size() || !subDirs.at(0).endsWith(".cue")) {
            songs+=dirSongs;
            if (songs.count()>=200){
                QCoreApplication::processEvents();
                QList<Song> *copy=new QList<Song>();
                *copy << songs;
                emit librarySongs(copy);
                songs.clear();
            }
        } else {
            DBUG << "IGNORING:" << dirSongs.size() << "track(s) as they are source files of cue?" << subDirs.at(0);
        }
        for (const QString &sub: subDirs) {
            recursivelyListDir(sub, songs);
        }

        if (topLevel && !songs.isEmpty()) {
            QList<Song> *copy=new QList<Song>();
            *copy << songs;
            emit librarySongs(copy);
        }
        return true;
    } else {
        return false;
    }
}

QStringList MPDConnection::getPlaylistFiles(const QString &name)
{
    QStringList files;
    Response response=sendCommand("listplaylistinfo "+encodeName(name));
    if (response.ok) {
        QList<Song> songs=MPDParseUtils::parseSongs(response.data, MPDParseUtils::Loc_Playlists);
        emit playlistInfoRetrieved(name, songs);
        for (const Song &s: songs) {
            files.append(s.file);
        }
    }
    return files;
}

QStringList MPDConnection::getAllFiles(const QString &dir)
{
    QStringList files;
    Response response=sendCommand("lsinfo "+encodeName(dir));
    if (response.ok) {
        QStringList subDirs;
        QList<Song> songs;
        MPDParseUtils::parseDirItems(response.data, details.dir, ver, songs, dir, subDirs, MPDParseUtils::Loc_Browse);
        for (const Song &song: songs) {
            if (Song::Playlist!=song.type) {
                files.append(song.file);
            }
        }
        for (const QString &sub: subDirs) {
            files+=getAllFiles(sub);
        }
    }

    return files;
}

bool MPDConnection::checkRemoteDynamicSupport()
{
    if (ver>=CANTATA_MAKE_VERSION(0,17,0)) {
        Response response;
        if (-1!=idleSocket.write("channels\n")) {
            idleSocket.waitForBytesWritten(constSocketCommsTimeout);
            response=readReply(idleSocket);
            if (response.ok) {
                return Utils::listToSet(MPDParseUtils::parseList(response.data, QByteArray("channel: "))).contains(constDynamicIn);
            }
        }
    }
    return false;
}

bool MPDConnection::subscribe(const QByteArray &channel)
{
    if (-1!=idleSocket.write("subscribe \""+channel+"\"\n")) {
        idleSocket.waitForBytesWritten(constSocketCommsTimeout);
        Response response=readReply(idleSocket);
        if (response.ok || response.data.startsWith("ACK [56@0]")) { // ACK => already subscribed...
            DBUG << "Created subscription to " << channel;
            return true;
        } else {
            DBUG << "Failed to subscribe to " << channel;
        }
    } else {
        DBUG << "Failed to create subscribe to " << channel;
    }
    return false;
}

void MPDConnection::setupRemoteDynamic()
{
    if (checkRemoteDynamicSupport()) {
        DBUG << "cantata-dynamic is running";
        if (subscribe(constDynamicOut)) {
            if (dynamicId.isEmpty()) {
                dynamicId=QHostInfo::localHostName().toLatin1()+'.'+QHostInfo::localDomainName().toLatin1()+'-'+QByteArray::number(QCoreApplication::applicationPid());
                if (!subscribe(constDynamicOut+'-'+dynamicId)) {
                    dynamicId.clear();
                }
            }
        }
    } else {
        DBUG << "remote dynamic is not supported";
    }
    emit dynamicSupport(!dynamicId.isEmpty());
}

void MPDConnection::readRemoteDynamicMessages()
{
    if (-1!=idleSocket.write("readmessages\n")) {
        idleSocket.waitForBytesWritten(constSocketCommsTimeout);
        Response response=readReply(idleSocket);
        if (response.ok) {
            MPDParseUtils::MessageMap messages=MPDParseUtils::parseMessages(response.data);
            if (!messages.isEmpty()) {
                QList<QByteArray> channels=QList<QByteArray>() << constDynamicOut << constDynamicOut+'-'+dynamicId;
                for (const QByteArray &channel: channels) {
                    if (messages.contains(channel)) {
                        for (const QString &m: messages[channel]) {
                            if (!m.isEmpty()) {
                                DBUG << "Received message " << m;
                                QStringList parts=m.split(':', CANTATA_SKIP_EMPTY);
                                QStringList message;
                                for (QString part: parts) {
                                    part=part.replace("{c}", ":");
                                    part=part.replace("{n}", "\n");
                                    part=part.replace("{cb}", "}");
                                    part=part.replace("{ob}", "{");
                                    part=part.replace("{q}", "\"");
                                    message.append(part);
                                }
                                emit dynamicResponse(message);
                            }
                        }
                    }
                }
            }
        }
    }
}

int MPDConnection::getVolume()
{
    Response response=sendCommand("status");
    if (response.ok) {
        MPDStatusValues sv=MPDParseUtils::parseStatus(response.data);
        return sv.volume;
    }
    return -1;
}

void MPDConnection::setRating(const QString &file, quint8 val)
{
    if (val>Song::Rating_Max) {
        return;
    }

    if (!canUseStickers) {
        emit error(tr("Cannot store ratings, as the 'sticker' MPD command is not supported."));
        return;
    }

    bool ok=0==val
                ? sendCommand("sticker delete song "+encodeName(file)+' '+constRatingSticker, 0!=val).ok
                : sendCommand("sticker set song "+encodeName(file)+' '+constRatingSticker+' '+quote(val)).ok;

    if (!ok && 0==val) {
        clearError();
    }

    if (ok) {
        emit rating(file, val);
    } else {
        getRating(file);
    }
}

void MPDConnection::setRating(const QStringList &files, quint8 val)
{
    if (1==files.count()) {
        setRating(files.at(0), val);
        return;
    }

    if (!canUseStickers) {
        emit error(tr("Cannot store ratings, as the 'sticker' MPD command is not supported."));
        return;
    }

    QList<QStringList> fileLists;
    if (files.count()>constMaxFilesPerAddCommand) {
        int numChunks=(files.count()/constMaxFilesPerAddCommand)+(files.count()%constMaxFilesPerAddCommand ? 1 : 0);
        for (int i=0; i<numChunks; ++i) {
            fileLists.append(files.mid(i*constMaxFilesPerAddCommand, constMaxFilesPerAddCommand));
        }
    } else {
        fileLists.append(files);
    }

    bool ok=true;
    for (const QStringList &list: fileLists) {
        QByteArray cmd = "command_list_begin\n";

        for (const QString &f: list) {
            if (0==val) {
                cmd+="sticker delete song "+encodeName(f)+' '+constRatingSticker+'\n';
            } else {
                cmd+="sticker set song "+encodeName(f)+' '+constRatingSticker+' '+quote(val)+'\n';
            }
        }

        cmd += "command_list_end";
        ok=sendCommand(cmd, 0!=val).ok;
        if (!ok) {
            break;
        }
    }

    if (!ok && 0==val) {
        clearError();
    }
}

void MPDConnection::getRating(const QString &file)
{
    quint8 r=0;
    if (canUseStickers) {
        Response resp=sendCommand("sticker get song "+encodeName(file)+' '+constRatingSticker, false);
        if (resp.ok) {
            QByteArray val=MPDParseUtils::parseSticker(resp.data, constRatingSticker);
            if (!val.isEmpty()) {
                r=val.toUInt();
            }
        } else { // Ignore errors about uknown sticker...
            clearError();
        }
        if (r>Song::Rating_Max) {
            r=0;
        }
    }
    emit rating(file, r);
}

void MPDConnection::getStickerSupport()
{
    Response response=sendCommand("commands");
    canUseStickers=response.ok &&
        Utils::listToSet(MPDParseUtils::parseList(response.data, QByteArray("command: "))).contains("sticker");
}

bool MPDConnection::fadingVolume()
{
    return volumeFade && QPropertyAnimation::Running==volumeFade->state();
}

bool MPDConnection::startVolumeFade()
{
    if (fadeDuration<=MinFade) {
        return false;
    }

    restoreVolume=getVolume();
    if (restoreVolume<5) {
        return false;
    }

    if (!volumeFade) {
        volumeFade = new QPropertyAnimation(this, "volume");
        volumeFade->setDuration(fadeDuration);
    }

    if (QPropertyAnimation::Running!=volumeFade->state()) {
        volumeFade->setStartValue(restoreVolume);
        volumeFade->setEndValue(-1);
        volumeFade->start();
    }
    return true;
}

void MPDConnection::stopVolumeFade()
{
    if (fadingVolume()) {
        volumeFade->stop();
        setVolume(restoreVolume);
        restoreVolume=-1;
    }
}

void MPDConnection::emitStatusUpdated(MPDStatusValues &v)
{
    if (restoreVolume>=0) {
        v.volume=restoreVolume;
    }
    #ifndef REPORT_MPD_ERRORS
    v.error=QString();
    #endif
    emit statusUpdated(v);
    if (!v.error.isEmpty()) {
        clearError();
    }
}

void MPDConnection::clearError()
{
    #ifdef REPORT_MPD_ERRORS
    if (isConnected()) {
        DBUG << __FUNCTION__;
        if (-1!=sock.write("clearerror\n")) {
            sock.waitForBytesWritten(500);
            readReply(sock);
        }
    }
    #endif
}

void MPDConnection::determineIfaceIp()
{
    static const QLatin1String ip4Local("127.0.0.1");
    if (!details.isLocal() && !details.hostname.isEmpty() && ip4Local!=details.hostname && QLatin1String("localhost")!=details.hostname) {
        QUdpSocket testSocket(this);
        testSocket.connectToHost(details.hostname, 1, QIODevice::ReadOnly);
        QString addr=testSocket.localAddress().toString();
        testSocket.close();
        if (!addr.isEmpty()) {
            DBUG << addr;
            emit ifaceIp(addr);
            return;
        }
    }
    DBUG << ip4Local;
    emit ifaceIp(ip4Local);
}

MpdSocket::MpdSocket(QObject *parent)
    : QObject(parent)
    , tcp(nullptr)
    , local(nullptr)
{
}

MpdSocket::~MpdSocket()
{
    deleteTcp();
    deleteLocal();
}

void MpdSocket::connectToHost(const QString &hostName, quint16 port, QIODevice::OpenMode mode)
{
    DBUG << "connectToHost" << hostName << port;
    if (hostName.startsWith('/') || hostName.startsWith('~')/* || hostName.startsWith('@')*/) {
        deleteTcp();
        if (!local) {
            local = new QLocalSocket(this);
            connect(local, SIGNAL(stateChanged(QLocalSocket::LocalSocketState)), this, SLOT(localStateChanged(QLocalSocket::LocalSocketState)));
            connect(local, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
        }
        DBUG << "Connecting to LOCAL socket";
        QString host = Utils::tildaToHome(hostName);
        /*if ('@'==host[0]) {
            host[0]='\0';
        }*/
        local->connectToServer(host, mode);
    } else {
        deleteLocal();
        if (!tcp) {
            tcp = new QTcpSocket(this);
            connect(tcp, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
            connect(tcp, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
        }
        DBUG << "Connecting to TCP socket";
        tcp->connectToHost(hostName, port, mode);
    }
}

void MpdSocket::localStateChanged(QLocalSocket::LocalSocketState state)
{
    emit stateChanged((QAbstractSocket::SocketState)state);
}

void MpdSocket::deleteTcp()
{
    if (tcp) {
        disconnect(tcp, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
        disconnect(tcp, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
        tcp->deleteLater();
        tcp=nullptr;
    }
}

void MpdSocket::deleteLocal()
{
    if (local) {
        disconnect(local, SIGNAL(stateChanged(QLocalSocket::LocalSocketState)), this, SLOT(localStateChanged(QLocalSocket::LocalSocketState)));
        disconnect(local, SIGNAL(readyRead()), this, SIGNAL(readyRead()));
        local->deleteLater();
        local=nullptr;
    }
}

// ONLY use this method to detect Non-MPD servers. The code which uses this will default to MPD
MPDServerInfo::ResponseParameter MPDServerInfo::lsinfoResponseParameters[] = {
    // github
    { "{lsinfo} Directory info not found for virtual-path '/", true, MPDServerInfo::ForkedDaapd, "forked-daapd" },
    // { "ACK [50@0] {lsinfo} Not found", false, MPDServerInfo::Mopidy, "Mopidy" },
    // ubuntu 16.10
    { "OK", false, MPDServerInfo::ForkedDaapd, "forked-daapd" }
};

void MPDServerInfo::detect() {
    MPDConnection *conn;

    if (!isUndetermined()) {
        return;
    }

    conn = MPDConnection::self();

    if (isUndetermined()) {
        MPDConnection::Response response=conn->sendCommand("stats");
        if (response.ok) {
            MPDStatsValues stats=MPDParseUtils::parseStats(response.data);
            if (0==stats.artists && 0==stats.albums && 0==stats.songs
                && 0==stats.uptime && 0==stats.playtime && 0==stats.dbPlaytime
                && 0==stats.dbUpdate) {
                setServerType(Mopidy);
                serverName = "Mopidy";
            }
        }
    }

    if (isUndetermined()) {
        MPDConnection::Response response=conn->sendCommand(lsinfoCommand, false, false);
        QList<QByteArray> lines = response.data.split('\n');
        bool match = false;
        unsigned int indx;
        for (const QByteArray &line: lines) {
            for (indx=0; indx<sizeof(lsinfoResponseParameters)/sizeof(ResponseParameter); ++indx) {
                ResponseParameter &rp = lsinfoResponseParameters[indx];
                if (rp.isSubstring) {
                    match = line.toLower().contains(rp.response.toLower());
                } else {
                    match = line.toLower() == rp.response.toLower();
                }
                if (match) {
                    setServerType(rp.serverType);
                    serverName = rp.name;
                    break;
                }
            }
            // first line is currently enough
            break;
        }
    }

    if (isUndetermined()) {
        // Default to MPD if cannot determine otherwise. Cantata is an *MPD* client first and foremost.
        setServerType(Mpd);
        serverName = "MPD";
    }

    DBUG << "detected serverType:" << getServerName() << "(" << getServerType() << ")";

    if (isMopidy()) {
        topLevelLsinfo = "lsinfo \"Local media\"";
    }

    if (isForkedDaapd()) {
        topLevelLsinfo = "lsinfo file:";

        QByteArray message = "sendmessage rating \"";
        message += "rating ";                           // sticker name
        message += QString().number(Song::Rating_Max).toUtf8();  // max rating
        message += " ";
        message += QString().number(Song::Rating_Step).toUtf8(); // rating step (optional)
        message += "\"";
        conn->sendCommand(message, false, false);
    }
}

void MPDServerInfo::reset() {
    setServerType(MPDServerInfo::Undetermined);
    serverName = "undetermined";
    topLevelLsinfo = "lsinfo";
}

#include "moc_mpdconnection.cpp"
