#include "TransfersModel.h"

#include "EventUpdater.h"
#include "mega/types.h"
#include "MegaApiSynchronizedRequest.h"
#include "MegaApplication.h"
#include "MegaTransferView.h"
#include "MessageDialogOpener.h"
#include "Platform.h"
#include "PowerOptions.h"
#include "ServiceUrls.h"
#include "SettingsDialog.h"
#include "ThreadPool.h"
#include "TransferItem.h"
#include "TransferMetaData.h"
#include "Utilities.h"
#include <MessageDialogOpener.h>

#include <QSharedData>

#include <algorithm>
#include <functional>
#include <utility>

using namespace mega;

static const QModelIndex DEFAULT_IDX = QModelIndex();

const qsizetype MAX_TRANSFERS = 2000;
const int CANCEL_THRESHOLD_THREAD = 100;
const qsizetype QUICK_CANCEL_THRESHOLD = 10000;
const qsizetype QUICK_CANCEL_MIN_THRESHOLD = 300;
const int QUICK_CANCEL_PERCENTAGE_THRESHOLD = 80;
const int START_THRESHOLD_THREAD = 50;
const int FAILED_THRESHOLD_THREAD = 100;
const int PAUSE_RESUME_THRESHOLD_THREAD = 300;
const int CLEAR_THRESHOLD_THREAD = 300;

//LISTENER THREAD
TransferThread::TransferThread() : mMaxTransfersToProcess(MAX_TRANSFERS)
{
    mDelegateListener = std::make_unique<QTMegaTransferListener>(MegaSyncApp->getMegaApi(), this);
    MegaSyncApp->getMegaApi()->addTransferListener(mDelegateListener.get());
}

TransferThread::TransfersToProcess TransferThread::processTransfers()
{
   TransfersToProcess transfers;
   if(mCacheMutex.tryLock())
   {
       qsizetype spaceForTransfers = mMaxTransfersToProcess;

       transfers.canceledTransfersByTag = extractFromCache(mTransfersToProcess.canceledTransfersByTag, spaceForTransfers);
       spaceForTransfers -= transfers.canceledTransfersByTag.size();

       transfers.failedFolderTransfersByTag = extractFromCache(mTransfersToProcess.failedFolderTransfersByTag, spaceForTransfers);
       spaceForTransfers -= transfers.failedFolderTransfersByTag.size();

       transfers.failedTransfersByTag = extractFromCache(mTransfersToProcess.failedTransfersByTag, spaceForTransfers);
       spaceForTransfers -= transfers.failedTransfersByTag.size();

       transfers.startTransfersByTag = extractFromCache(mTransfersToProcess.startTransfersByTag, spaceForTransfers);
       spaceForTransfers -= transfers.startTransfersByTag.size();

       transfers.startSyncTransfersByTag = extractFromCache(mTransfersToProcess.startSyncTransfersByTag, spaceForTransfers);
       spaceForTransfers -= transfers.startSyncTransfersByTag.size();

       transfers.updateTransfersByTag = extractFromCache(mTransfersToProcess.updateTransfersByTag, spaceForTransfers);

       mCacheMutex.unlock();
   }

   return transfers;
}

void TransferThread::clear()
{
    QMutexLocker lock(&mCacheMutex);

    mTransfersToProcess.clear();
    mTransfersCount.clear();
    mLastTransfersCount.clear();
}

QList<QExplicitlySharedDataPointer<TransferData>>
    TransferThread::extractFromCache(QMap<int, QExplicitlySharedDataPointer<TransferData>>& dataMap,
                                     qsizetype spaceForTransfers)
{
    if(!dataMap.isEmpty() && spaceForTransfers > 0)
    {
        if(dataMap.size() > spaceForTransfers)
        {
            QList<QExplicitlySharedDataPointer<TransferData>> auxList;
            for(auto index = 0; index < spaceForTransfers
                && !dataMap.isEmpty(); ++index)
            {
                auto& firstItem = dataMap.first();
                if(firstItem)
                {
                    auxList.append(firstItem);
                    dataMap.take(firstItem->mTag);
                }
            }

            return auxList;
        }
        else
        {
            auto auxList = dataMap.values();
            dataMap.clear();

            return auxList;
        }
    }

    return QList<QExplicitlySharedDataPointer<TransferData>>();
}

QExplicitlySharedDataPointer<TransferData> TransferThread::createData(MegaTransfer *transfer, MegaError* e)
{
    QExplicitlySharedDataPointer<TransferData> d (new TransferData(transfer));
    updateFailedTransfer(d, transfer, e);

    return d;
}

QExplicitlySharedDataPointer<TransferData> TransferThread::checkIfRepeatedAndSubstituteInStartTransfers(QMap<int, QExplicitlySharedDataPointer<TransferData>>& dataMap, MegaTransfer* transfer)
{
    if(dataMap.contains(transfer->getTag()))
    {
        auto item = dataMap.value(transfer->getTag());
        if(transfer->getState() == mega::MegaTransfer::STATE_CANCELLED)
        {
            dataMap.remove(transfer->getTag());
            return QExplicitlySharedDataPointer<TransferData>();
        }

        if(item->mNotificationNumber < transfer->getNotificationNumber())
        {
            dataMap[transfer->getTag()] = createData(transfer, nullptr);
            return dataMap[transfer->getTag()];
        }

        return item;
    }

    return QExplicitlySharedDataPointer<TransferData>();
}

QExplicitlySharedDataPointer<TransferData> TransferThread::checkIfRepeatedAndSubstitute(QMap<int, QExplicitlySharedDataPointer<TransferData>>& dataMap, MegaTransfer* transfer)
{
    if(dataMap.contains(transfer->getTag()))
    {
        auto item = dataMap.value(transfer->getTag());
        if(item->mNotificationNumber < transfer->getNotificationNumber())
        {
            dataMap[transfer->getTag()] = createData(transfer, nullptr);
            return dataMap[transfer->getTag()];
        }

        return item;
    }

    return QExplicitlySharedDataPointer<TransferData>();
}

QExplicitlySharedDataPointer<TransferData> TransferThread::checkIfRepeatedAndRemove(QMap<int, QExplicitlySharedDataPointer<TransferData>>& dataMap, MegaTransfer* transfer)
{
    if(dataMap.contains(transfer->getTag()))
    {
        auto item = dataMap.value(transfer->getTag());
        if(item->mNotificationNumber < transfer->getNotificationNumber())
        {
            dataMap.remove(transfer->getTag());
            return QExplicitlySharedDataPointer<TransferData>();
        }

        return item;
    }

    return QExplicitlySharedDataPointer<TransferData>();
}

QExplicitlySharedDataPointer<TransferData> TransferThread::onTransferEvent(MegaTransfer *transfer, mega::MegaError* e)
{
    auto result = checkIfRepeatedAndSubstituteInStartTransfers(mTransfersToProcess.startTransfersByTag, transfer);

    if(!result)
    {
        result = checkIfRepeatedAndSubstitute(mTransfersToProcess.startSyncTransfersByTag, transfer);
    }

    if(!result)
    {
        result = checkIfRepeatedAndSubstitute(mTransfersToProcess.canceledTransfersByTag, transfer);
    }

    if(!result)
    {
        result = checkIfRepeatedAndSubstitute(mTransfersToProcess.failedFolderTransfersByTag, transfer);
    }

    if(!result)
    {
        result = checkIfRepeatedAndSubstitute(mTransfersToProcess.failedTransfersByTag, transfer);
    }

    if(!result)
    {
        result = checkIfRepeatedAndRemove(mTransfersToProcess.updateTransfersByTag, transfer);
    }

    updateFailedTransfer(result, transfer, e);

    return result;
}

void TransferThread::updateFailedTransfer(QExplicitlySharedDataPointer<TransferData> data,
                                          mega::MegaTransfer *transfer,
                                          mega::MegaError *e)
{
    if(transfer->getState() == MegaTransfer::STATE_FAILED || (e && e->getErrorCode() != mega::MegaError::API_OK))
    {
        if(data && !data->mFailedTransfer)
        {
            data->mFailedTransfer = std::shared_ptr<mega::MegaTransfer>(transfer->copy());
        }
    }
}

void TransferThread::onTransferStart(MegaApi *, MegaTransfer *transfer)
{
    if(isIgnored(transfer))
    {
        return;
    }
    auto isTemp = isTempTransfer(transfer, false);
    //If it is temp, we don´t track it with the TransferMetaData
    if(!isTemp)
    {
        //These type of transfers are not added to TransferMetaData item
        if(!transfer->isSyncTransfer() && !transfer->isBackupTransfer() && !transfer->isStreamingTransfer())
        {
            TransferMetaDataContainer::start(transfer);
        }
    }

    if(!transfer->isStreamingTransfer()
                && !transfer->isFolderTransfer())
        {
            //If it is temp, we don´t add it to the counters
            if(!isTemp)
            {
                QMutexLocker counterLock(&mCountersMutex);
                auto fileType = Utilities::getFileType(QString::fromUtf8(transfer->getFileName()),
                                                       Utilities::AttributeType::NONE);
                mTransfersCount.transfersByType[fileType]++;

                if(transfer->getType() == MegaTransfer::TYPE_UPLOAD)
                {
                    mTransfersCount.totalUploads++;
                    mTransfersCount.addPendingUpload(transfer);
                    mTransfersCount.totalUploadBytes += transfer->getTotalBytes();
                    mTransfersCount.completedUploadBytes += transfer->getTransferredBytes();

                    mLastTransfersCount.totalUploads++;
                    mLastTransfersCount.addPendingUpload(transfer);
                    mLastTransfersCount.totalUploadBytes += transfer->getTotalBytes();
                    mLastTransfersCount.completedUploadBytes += transfer->getTransferredBytes();
                }
                else
                {
                    mTransfersCount.totalDownloads++;
                    mTransfersCount.addPendingDownload(transfer);
                    mTransfersCount.totalDownloadBytes += transfer->getTotalBytes();
                    mTransfersCount.completedDownloadBytes += transfer->getTransferredBytes();

                    mLastTransfersCount.totalDownloads++;
                    mLastTransfersCount.addPendingDownload(transfer);
                    mLastTransfersCount.totalDownloadBytes += transfer->getTotalBytes();
                    mLastTransfersCount.completedDownloadBytes += transfer->getTransferredBytes();
                }
            }

            {
                QMutexLocker cacheLock(&mCacheMutex);
                auto data = onTransferEvent(transfer,  nullptr);

                if(!data)
                {
                    data = createData(transfer, nullptr);

                    trackTransfer(data);

                    if(transfer->isSyncTransfer())
                    {
                        mTransfersToProcess.startSyncTransfersByTag.insert(transfer->getTag(), data);
                    }
                    else
                    {
                        mTransfersToProcess.startTransfersByTag.insert(transfer->getTag(), data);
                    }
                }

                data->mIsTempTransfer = isTemp;
            }
        }

}

void TransferThread::onTransferUpdate(MegaApi *, MegaTransfer *transfer)
{
    if (!transfer->isStreamingTransfer()
            && !transfer->isFolderTransfer())
    {
        //If it is temp, we ignore the updates
        auto isTemp = isTempTransfer(transfer, false);
        if(isTemp || isIgnored(transfer))
        {
            return;
        }

        {
            QMutexLocker counterLock(&mCountersMutex);
            if(transfer->getType() == MegaTransfer::TYPE_UPLOAD)
            {
                mTransfersCount.completedUploadBytes += transfer->getDeltaSize();
                mLastTransfersCount.completedUploadBytes += transfer->getDeltaSize();
            }
            else
            {
                mTransfersCount.completedDownloadBytes += transfer->getDeltaSize();
                mLastTransfersCount.completedDownloadBytes += transfer->getDeltaSize();
            }
        }

        {
            QMutexLocker cacheLock(&mCacheMutex);
            auto data = onTransferEvent(transfer, nullptr);

            if(!data)
            {
                data =  createData(transfer, nullptr);

                mTransfersToProcess.updateTransfersByTag.insert(transfer->getTag(), data);
            }
        }
    }
}

void TransferThread::onTransferFinish(MegaApi* megaApi, MegaTransfer *transfer, MegaError* e)
{
    if (!transfer->isStreamingTransfer())
    { 
        if(isIgnored(transfer, true))
        {
            return;
        }

        auto isTemp = isTempTransfer(transfer, true);

        //If it is temp, we don´t add it to the counters
        if(!isTemp)
        {
            //This method is run in other thread, but all the logic related to TransferMetaData should be run in the GUI thread
            auto idResult = TransferMetaDataContainer::appDataToId(transfer->getAppData());
            if (idResult.first || transfer->getFolderTransferTag() > 0)
            {
                if(idResult.first)
                {
                    TransferMetaDataContainer::finish(idResult.second, transfer, e);
                }
                else if(transfer->getFolderTransferTag() > 0)
                {
                    //If it is a completed transfer from a retried folder, ignore it
                    TransferMetaDataContainer::finishFromFolderTransfer(transfer, e);
                }
            }

            if(!megaApi->isLoggedIn())
            {
                return;
            }

            if(!transfer->isFolderTransfer())
            {
                {
                    QMutexLocker counterLock(&mCountersMutex);
                    auto fileType =
                        Utilities::getFileType(QString::fromUtf8(transfer->getFileName()),
                                               Utilities::AttributeType::NONE);

                    if (transfer->getState() == MegaTransfer::STATE_CANCELLED ||
                        (transfer->getState() == MegaTransfer::STATE_FAILED &&
                         transfer->isSyncTransfer()))
                    {
                        mTransfersCount.transfersByType[fileType]--;

                        if(transfer->getType() == MegaTransfer::TYPE_UPLOAD)
                        {
                            mTransfersCount.completedUploadBytes -= transfer->getTransferredBytes();
                            mTransfersCount.totalUploadBytes -= transfer->getTotalBytes();
                            mTransfersCount.removePendingUpload(transfer);
                            mTransfersCount.totalUploads--;

                            mLastTransfersCount.completedUploadBytes -= transfer->getTransferredBytes();
                            mLastTransfersCount.totalUploadBytes -= transfer->getTotalBytes();
                            mLastTransfersCount.removePendingUpload(transfer);
                            mLastTransfersCount.totalUploads--;
                        }
                        else
                        {
                            mTransfersCount.completedDownloadBytes -= transfer->getTransferredBytes();
                            mTransfersCount.totalDownloadBytes -= transfer->getTotalBytes();
                            mTransfersCount.removePendingDownload(transfer);
                            mTransfersCount.totalDownloads--;

                            mLastTransfersCount.completedDownloadBytes -= transfer->getTransferredBytes();
                            mLastTransfersCount.totalDownloadBytes -= transfer->getTotalBytes();
                            mLastTransfersCount.removePendingDownload(transfer);
                            mLastTransfersCount.totalDownloads--;
                        }

                        if(mTransfersCount.pendingTransfers() == 0)
                        {
                            mLastTransfersCount.clear();
                        }
                    }
                    else
                    {
                        mTransfersCount.transfersFinishedByType[fileType]++;
                        if(transfer->getType() == MegaTransfer::TYPE_UPLOAD)
                        {
                            mTransfersCount.removePendingUpload(transfer);
                            mLastTransfersCount.removePendingUpload(transfer);

                            if(transfer->getTransferredBytes() < transfer->getTotalBytes())
                            {
                                mTransfersCount.completedUploadBytes += transfer->getDeltaSize();
                                mLastTransfersCount.completedUploadBytes += transfer->getDeltaSize();
                            }

                            if (transfer->getState() == MegaTransfer::STATE_FAILED)
                            {
                                mTransfersCount.failedUploads++;
                                mLastTransfersCount.failedUploads++;
                            }

                            mLastTransfersCount.completedUploadsByTag.insert(transfer->getTag());
                        }
                        else
                        {
                            mTransfersCount.removePendingDownload(transfer);
                            mLastTransfersCount.removePendingDownload(transfer);

                            if(transfer->getTransferredBytes() < transfer->getTotalBytes())
                            {
                                mTransfersCount.completedDownloadBytes += transfer->getDeltaSize();
                                mLastTransfersCount.completedDownloadBytes += transfer->getDeltaSize();
                            }

                            if (transfer->getState() == MegaTransfer::STATE_FAILED)
                            {
                                mTransfersCount.failedDownloads++;
                                mLastTransfersCount.failedDownloads++;
                            }

                            mLastTransfersCount.completedDownloadsByTag.insert(transfer->getTag());
                        }

                        if(mTransfersCount.pendingTransfers() == 0)
                        {
                            mLastTransfersCount.clear();
                        }
                    }
                }

            }
        }

        {
            if(!megaApi->isLoggedIn())
            {
                return;
            }

            QMutexLocker cacheLock(&mCacheMutex);
            auto data = onTransferEvent(transfer, e);

            if(!data)
            {
                data =  createData(transfer, e);

                if(transfer->isFolderTransfer())
                {
                    if(transfer->getState() == MegaTransfer::STATE_FAILED
                            || e->getErrorCode() != mega::MegaError::API_OK)
                    {
                        //In some scenarios, the error code can be different to API_OK but the state is not failed
                        data->setState(TransferData::TRANSFER_FAILED);
                        mTransfersToProcess.failedFolderTransfersByTag.insert(transfer->getTag(), data);
                    }
                }
                else
                {
                    if(transfer->getState() == MegaTransfer::STATE_CANCELLED)
                    {
                        mTransfersToProcess.canceledTransfersByTag.insert(transfer->getTag(), data);
                    }
                    else if(transfer->getState() == MegaTransfer::STATE_FAILED
                            || e->getErrorCode() != mega::MegaError::API_OK)
                    {
                        mTransfersToProcess.failedTransfersByTag.insert(transfer->getTag(), data);
                    }
                    else
                    {
                        mTransfersToProcess.updateTransfersByTag.insert(transfer->getTag(), data);
                    }
                }
            }

            trackTransfer(data);

            data->mIsTempTransfer = isTemp;
        }
    }
}

void TransferThread::onTransferTemporaryError(MegaApi*, MegaTransfer *transfer, MegaError* e)
{

    if (!transfer->isStreamingTransfer()
            && !transfer->isFolderTransfer())
    {
        //If it is temp, we ignore the temporary errors
        auto isTemp = isTempTransfer(transfer, false);
        if(isTemp || isIgnored(transfer))
        {
            return;
        }

        {
            QMutexLocker counterLock(&mCountersMutex);
            if(transfer->getType() == MegaTransfer::TYPE_UPLOAD)
            {
                mTransfersCount.completedUploadBytes += transfer->getDeltaSize();
                mLastTransfersCount.completedUploadBytes += transfer->getDeltaSize();
            }
            else
            {
                mTransfersCount.completedDownloadBytes += transfer->getDeltaSize();
                mLastTransfersCount.completedDownloadBytes += transfer->getDeltaSize();
            }
        }

        {
            QMutexLocker cacheLock(&mCacheMutex);
            auto data = onTransferEvent(transfer, nullptr);
            if(!data)
            {
                data =  createData(transfer, e);

                mTransfersToProcess.updateTransfersByTag.insert(transfer->getTag(), data);
                data->mTemporaryError = true;
            }
        }
    }
}

bool TransferThread::isCompletedFromFolderRetry(mega::MegaTransfer *transfer)
{
    if (transfer->getFolderTransferTag() > 0)
    {
        if(transfer->getType() == mega::MegaTransfer::TYPE_UPLOAD)
        {
            auto data = TransferMetaDataContainer::getAppDataByFolderTransferTag<UploadTransferMetaData>(transfer->getFolderTransferTag());
            if(data)
            {
                if(data->hasBeenPreviouslyCompleted(transfer))
                {
                    mIgnoredFiles.append(transfer->getTag());
                    return true;
                }
            }
        }
        else
        {
            auto data = TransferMetaDataContainer::getAppDataByFolderTransferTag<DownloadTransferMetaData>(transfer->getFolderTransferTag());
            if(data)
            {
                if(data->hasBeenPreviouslyCompleted(transfer))
                {
                    mIgnoredFiles.append(transfer->getTag());
                    return true;
                }
            }
        }
    }

    return false;
}

bool TransferThread::isRetried(mega::MegaTransfer *transfer)
{
    if(transfer->isFolderTransfer())
    {
        if(transfer->getFolderTransferTag() <= 0)
        {
            return isRetriedFolder(transfer);
        }
    }
    else if(transfer->getFolderTransferTag() > 0 && mRetriedFolder.contains(transfer->getFolderTransferTag()))
    {
        return isCompletedFromFolderRetry(transfer);
    }

    return false;
}

bool TransferThread::isRetriedFolder(mega::MegaTransfer *transfer)
{
    auto appDataId = TransferMetaDataContainer::appDataToId(transfer->getAppData());
    if(appDataId.first)
    {
        auto data = TransferMetaDataContainer::getAppDataById(appDataId.second);
        if(data)
        {
            if(data->isRetriedFolder(transfer))
            {
                mRetriedFolder.append(transfer->getTag());
                return true;
            }
        }
    }

    return false;
}

bool TransferThread::isIgnored(mega::MegaTransfer *transfer, bool removeCache)
{
    if(!transfer->isFolderTransfer())
    {
        if(mIgnoredFiles.contains(transfer->getTag()))
        {
            if(removeCache)
            {
                mIgnoredFiles.removeOne(transfer->getTag());
                isTempTransfer(transfer, removeCache);
            }

            return true;
        }
    }
    else
    {
        if(removeCache)
        {
            mRetriedFolder.removeOne(transfer->getTag());
        }
    }

    return false;
}

bool TransferThread::isTempTransfer(mega::MegaTransfer *transfer, bool removeCache)
{
    if(transfer->getType() == mega::MegaTransfer::TYPE_DOWNLOAD)
    {
        if(QString::fromUtf8(transfer->getParentPath()) == Preferences::instance()->getTempTransfersPath())
        {
            if(removeCache)
            {
                QFile fileToRemove(QString::fromUtf8(transfer->getPath()));
                if(fileToRemove.exists())
                {
                    fileToRemove.remove();
                }
            }

            return true;
        }
    }

    return false;
}

TransfersCount TransferThread::getTransfersCount()
{
    QMutexLocker lock(&mCountersMutex);
    return mTransfersCount;
}

LastTransfersCount TransferThread::getLastTransfersCount()
{
    QMutexLocker lock(&mCountersMutex);
    return mLastTransfersCount;
}

int TransfersModel::hasActiveTransfers() const
{
    return static_cast<int>(mActiveTransfers.size());
}

void TransfersModel::setActiveTransfer(TransferTag tag)
{
    auto isEmpty = mActiveTransfers.isEmpty();
    mActiveTransfers.insert(tag);
    if(isEmpty)
    {
        emit activeTransfersChanged();
    }
}

void TransfersModel::unsetActiveTransfer(TransferTag tag)
{
    auto removed = mActiveTransfers.remove(tag);
    if(removed && mActiveTransfers.isEmpty())
    {
        emit activeTransfersChanged();
    }
}

void TransfersModel::checkActiveTransfer(TransferTag tag, bool isActive)
{
   isActive ? setActiveTransfer(tag) : unsetActiveTransfer(tag);
}

void TransfersModel::uiUnblocked()
{
    showSyncCancelledWarning();
}

void TransferThread::resetUploads(
    QList<QExplicitlySharedDataPointer<TransferData>> transfersToReset)
{
    QMutexLocker lock(&mCountersMutex);

    foreach(auto& transfer, transfersToReset)
    {
        if(mTransfersCount.totalUploads > 0)
        {
            mTransfersCount.totalUploads--;
            mTransfersCount.totalUploadBytes -= transfer->mTotalSize;
            mTransfersCount.transfersByType[transfer->mFileType]--;
            mTransfersCount.transfersFinishedByType[transfer->mFileType]--;

            if(transfer->isFailed() && !transfer->isSyncTransfer())
            {
                mTransfersCount.failedUploads--;
            }

            if(transfer->isFailed())
            {
                mTransfersCount.completedUploadBytes -= transfer->mTransferredBytes;
            }
            else
            {
                mTransfersCount.completedUploadBytes -= transfer->mTotalSize;
            }
        }

        if(mLastTransfersCount.completedUploadsByTag.contains(transfer->mTag))
        {
            mLastTransfersCount.completedUploadsByTag.remove(transfer->mTag);
            mLastTransfersCount.totalUploads--;
            mLastTransfersCount.completedUploadBytes -= transfer->mTotalSize;
            mLastTransfersCount.totalUploadBytes -= transfer->mTotalSize;
            mLastTransfersCount.transfersByType[transfer->mFileType]--;
            mLastTransfersCount.transfersFinishedByType[transfer->mFileType]--;

            if(transfer->isFailed() && !transfer->isSyncTransfer())
            {
                mLastTransfersCount.failedUploads--;
            }

            if(transfer->isFailed())
            {
                mLastTransfersCount.completedUploadBytes -= transfer->mTransferredBytes;
            }
            else
            {
                mLastTransfersCount.completedUploadBytes -= transfer->mTotalSize;
            }
        }
    }
}

void TransferThread::resetDownloads(
    QList<QExplicitlySharedDataPointer<TransferData>> transfersToReset)
{
    QMutexLocker lock(&mCountersMutex);

    foreach(auto& transfer, transfersToReset)
    {
        if(mTransfersCount.totalDownloads > 0)
        {
            mTransfersCount.totalDownloads--;
            mTransfersCount.totalDownloadBytes -= transfer->mTotalSize;
            mTransfersCount.transfersByType[transfer->mFileType]--;
            mTransfersCount.transfersFinishedByType[transfer->mFileType]--;

            if (transfer->isFailed() && !transfer->isSyncTransfer())
            {
                mTransfersCount.failedDownloads--;
            }
            else
            {
                mTransfersCount.completedDownloadBytes -= transfer->mTotalSize;
            }
        }

        if(mLastTransfersCount.completedDownloadsByTag.contains(transfer->mTag))
        {
            mLastTransfersCount.completedDownloadsByTag.remove(transfer->mTag);
            mLastTransfersCount.totalDownloads--;
            mLastTransfersCount.totalDownloadBytes -= transfer->mTotalSize;
            mLastTransfersCount.transfersByType[transfer->mFileType]--;
            mLastTransfersCount.transfersFinishedByType[transfer->mFileType]--;

            if (transfer->isFailed() && !transfer->isSyncTransfer())
            {
                mLastTransfersCount.failedDownloads--;
            }
            else
            {
                mLastTransfersCount.completedDownloadBytes -= transfer->mTotalSize;
            }
        }
    }
}

void TransferThread::setMaxTransfersToProcess(int max)
{
    mMaxTransfersToProcess = max;
}

void TransferThread::trackTransfer(QExplicitlySharedDataPointer<TransferData> data)
{
    QMutexLocker lock(&mTrackTransferMutex);
    foreach(auto track, mTrackToTransfers)
    {
        track->checkTransfer(data);

        if (track->areAllTransfersFinished())
        {
            removeFinishedTracks(track->id());
        }
    }
}

std::shared_ptr<TransferTrack> TransferThread::addTrackToTransfer(const QString& id,
                                                                  TransferData::TransferType type)
{
    QMutexLocker lock(&mTrackTransferMutex);

    auto& track(mTrackToTransfers[id]);

    if (!track)
    {
        track = std::make_shared<TransferTrack>(id, type);
        mTrackToTransfers.insert(id, track);
    }

    return track;
}

std::shared_ptr<TransferTrack> TransferThread::getTrackToTransfer(const QString& id)
{
    QMutexLocker lock(&mTrackTransferMutex);
    return mTrackToTransfers.value(id, nullptr);
}

void TransferThread::removeTrackToTransfer(const QString& id)
{
    QMutexLocker lock(&mTrackTransferMutex);
    removeFinishedTracks(id);
}

void TransferThread::removeFinishedTracks(const QString& id)
{
    // No need of mutex as it is used from the caller
    mTrackToTransfers.remove(id);
}

///////////////// TRANSFERS MODEL //////////////////////////////////////////////

const int PROCESS_TIMER = 100;
const int RESET_AFTER_EMPTY_RECEIVES = 10;
const int MODEL_HAS_CHANGED_AFTER_EMPTY_RECEIVES = 5;

TransfersModel::TransfersModel():
    QAbstractItemModel(),
    mMegaApi(MegaSyncApp->getMegaApi()),
    mPreferences(Preferences::instance()),
    mTransfersProcessChanged(0),
    mUpdateMostPriorityTransfer(0),
    mUiBlockedCounter(0),
    mUiBlockedByCounter(0),
    mCancelledFrom(nullptr),
    mSyncsInRowsToCancel(false),
    mIgnoreMoveSignal(false)
{
    qRegisterMetaType<QList<QPersistentModelIndex>>("QList<QPersistentModelIndex>");
    qRegisterMetaType<QAbstractItemModel::LayoutChangeHint>("QAbstractItemModel::LayoutChangeHint");
    qRegisterMetaType<QVector<int>>("QVector<int>");
    qRegisterMetaType<QVector<int>>("QSet<int>");
    qRegisterMetaType<TransferItem>("TransferItem");

    mAreAllPaused = mPreferences->getGlobalPaused();
    mMegaApi->pauseTransfers(mAreAllPaused);

    mTransferEventThread = new QThread();
    mTransferEventWorker = new TransferThread();
    mTransferEventWorker->moveToThread(mTransferEventThread);

    //Update transfers state for the first time
    updateTransfersCount();

    mProcessTransfersTimer.setInterval(PROCESS_TIMER);
    QObject::connect(&mProcessTransfersTimer, &QTimer::timeout, this, &TransfersModel::onProcessTransfers);
    mProcessTransfersTimer.start();

    mTransferEventThread->start();

    mMostPriorityTransferTimer.setInterval(100);
    mMostPriorityTransferTimer.setSingleShot(true);
    QObject::connect(&mMostPriorityTransferTimer, &QTimer::timeout, this, &TransfersModel::askForMostPriorityTransfer);

    connect(&mUpdateTransferWatcher, &QFutureWatcher<void>::finished, this, &TransfersModel::onUpdateTransfersFinished);
    connect(&mClearTransferWatcher, &QFutureWatcher<void>::finished, this, &TransfersModel::onClearTransfersFinished);
    connect(&mAskForMostPriorityTransfersWatcher, &QFutureWatcher<QPair<int,int>>::finished, this, &TransfersModel::onAskForMostPriorityTransfersFinished);

    connect(mTransferEventThread, &QThread::finished, mTransferEventThread, &QObject::deleteLater, Qt::DirectConnection);
    connect(mTransferEventThread, &QThread::finished, mTransferEventWorker, &QObject::deleteLater, Qt::DirectConnection);

    connect(this, &TransfersModel::activeTransfersChanged, this, &TransfersModel::onKeepPCAwake);

    // Use in signal, so we need to register it
    qRegisterMetaType<TransferData::TransferState>("TransferData::TransferState");
}

TransfersModel::~TransfersModel()
{
    mActiveTransfers.clear();
    onKeepPCAwake();

    // Cleanup
    mTransfers.clear();

    mTransferEventThread->quit();
    mTransferEventThread->wait();
    mTransferEventThread->deleteLater();
}

void TransfersModel::pauseModelProcessing(bool value)
{
    QMutexLocker lock(&mModelMutex);

    if(value)
    {
        mProcessTransfersTimer.stop();
    }
    else
    {
        mProcessTransfersTimer.start(PROCESS_TIMER);
    }
}

bool TransfersModel::areAllPaused() const
{
    return mAreAllPaused;
}

std::shared_ptr<TransferTrack> TransfersModel::addTrackToTransfer(const QString& id,
                                                                  TransferData::TransferType type)
{
    return mTransferEventWorker->addTrackToTransfer(id, type);
}

std::shared_ptr<TransferTrack> TransfersModel::getTrackToTransfer(const QString& id)
{
    return mTransferEventWorker->getTrackToTransfer(id);
}

void TransfersModel::removeTrackToTransfer(const QString& id)
{
    mTransferEventWorker->removeTrackToTransfer(id);
}

const QExplicitlySharedDataPointer<const TransferData>
    TransfersModel::downloadTransferFound(DownloadTransferInfo* info) const
{
    QExplicitlySharedDataPointer<const TransferData> foundTransfer;

    auto transfers = getTransfersToIterate();

    foreach(auto transfer, transfers)
    {
        if(transfer && !transfer->isUpload())
        {
            if (transfer->mNodeHandle == info->nodeHandle && transfer->getState() & info->state)
            {
                foundTransfer = transfer;
                break;
            }
        }
    }

    return foundTransfer;
}

const QExplicitlySharedDataPointer<const TransferData> TransfersModel::activeUploadTransferFound(UploadTransferInfo* info) const
{
    QExplicitlySharedDataPointer<const TransferData> foundTransfer;

    auto transfers = getTransfersToIterate();

    foreach(auto transfer, transfers)
    {
        if(transfer && transfer->isUpload())
        {
            if (transfer->mFilename.compare(info->filename, Qt::CaseSensitive) == 0 &&
                transfer->mParentHandle == info->parentHandle &&
                transfer->path() == info->localPath && transfer->getState() & info->state)
            {
                foundTransfer = transfer;
                break;
            }
        }
    }

    return foundTransfer;
}

bool TransfersModel::hasChildren(const QModelIndex& parent) const
{
    if (parent == DEFAULT_IDX)
    {
        return !mTransfers.empty();
    }
    return false;
}

int TransfersModel::rowCount(const QModelIndex& parent) const
{
    int rowCount (0);
    if (parent == DEFAULT_IDX)
    {
        rowCount = static_cast<int>(mTransfers.size());
    }
    return rowCount;
}

int TransfersModel::columnCount(const QModelIndex& parent) const
{
    //The same number of columns as sort criterions are needed
    //However in the sort filter the column count WILL BE ALWAYS 1 (check columnCount on sort filter class)
    if (parent == DEFAULT_IDX)
    {
        return static_cast<int>(SortCriterion::LAST);
    }
    return 0;
}

QVariant TransfersModel::data(const QModelIndex& index, int role) const
{
    if (role == Qt::DisplayRole)
    {
        return QVariant::fromValue(TransferItem(getTransfer(index.row())));
    }

    return QVariant();
}

QModelIndex TransfersModel::parent(const QModelIndex&) const
{
    return DEFAULT_IDX;
}

QModelIndex TransfersModel::index(int row, int column, const QModelIndex&) const
{
    return (row < rowCount(DEFAULT_IDX)) ?  createIndex(row, column) : DEFAULT_IDX;
}

void TransfersModel::onProcessTransfers()
{
    if(mTransfersToProcess.isEmpty())
    {
        mTransfersToProcess = mTransferEventWorker->processTransfers();
    }

    if(!mTransfersToProcess.isEmpty())
    {
        mostPriorityTransferMayChanged(true);

        bool asynchronousProcessed(false);

        const auto containsTransfersToStart(mTransfersToProcess.startTransfersByTag.size());
        const auto containsSyncTransfersToStart(mTransfersToProcess.startSyncTransfersByTag.size());
        const auto containsTransfersToUpdate(mTransfersToProcess.updateTransfersByTag.size());
        const auto containsTransfersToCancel(mTransfersToProcess.canceledTransfersByTag.size());
        const auto containsFolderTransfersFailed(
            mTransfersToProcess.failedFolderTransfersByTag.size());
        const auto containsTransfersFailed(mTransfersToProcess.failedTransfersByTag.size());

        if(containsTransfersToCancel > 0)
        {            
            cacheCancelTransfersTags();

            if(isUiBlockedModeActive() || containsTransfersToCancel > CANCEL_THRESHOLD_THREAD)
            {
                setUiBlockedMode(true);
            }
            else if(!isUiBlockedModeActive())
            {
                if(mModelMutex.tryLock())
                {
                    processCancelTransfers();
                    showSyncCancelledWarning();
                    mModelMutex.unlock();
                }
            }
        }

        if(containsFolderTransfersFailed > 0)
        {
            if (mModelMutex.tryLock())
            {
                const auto pending =
                    std::exchange(mTransfersToProcess.failedFolderTransfersByTag, {});
                mFailedFoldersByTag.reserve(mFailedFoldersByTag.size() + pending.size());
                for (const auto& transfer: pending)
                {
                    mFailedFoldersByTag.insert(transfer->mTag, transfer);
                }
                mModelMutex.unlock();
            }
        }

        if(containsTransfersFailed > 0)
        {
            if(isUiBlockedModeActive() || containsTransfersFailed > FAILED_THRESHOLD_THREAD)
            {
                setUiBlockedMode(true);
                asynchronousProcessed = true;

                auto future = QtConcurrent::run([this](){
                    if(mModelMutex.tryLock())
                    {
                        blockModelSignals(true);
                        processFailedTransfers();
                        blockModelSignals(false);

                        mModelMutex.unlock();
                    }
                });
                mUpdateTransferWatcher.setFuture(future);
            }
            else
            {
                if(mModelMutex.tryLock())
                {
                    processFailedTransfers();
                    mModelMutex.unlock();
                }

                processSyncFailedTransfers();
            }
        }
        else
        {
            if(containsTransfersToStart > 0 || containsSyncTransfersToStart > 0)
            {
                if(isUiBlockedModeActive() || containsTransfersToStart > START_THRESHOLD_THREAD)
                {
                    setUiBlockedMode(true);
                }

                if(mModelMutex.tryLock())
                {
                    if(isUiBlockedModeActive())
                    {
                        blockModelSignals(true);
                    }

                    processStartTransfers(mTransfersToProcess.startTransfersByTag);
                    processStartTransfers(mTransfersToProcess.startSyncTransfersByTag);

                    if(isUiBlockedModeActive())
                    {
                        blockModelSignals(false);
                    }

                    mModelMutex.unlock();
                }
            }
            else if(containsTransfersToUpdate > 0)
            {
                if(isUiBlockedByCounter())
                {
                    asynchronousProcessed = true;

                    auto future = QtConcurrent::run([this, containsTransfersToUpdate](){
                        if(mModelMutex.tryLock())
                        {
                            blockModelSignals(true);
                            processUpdateTransfers();
                            blockModelSignals(false);

                            updateUiBlockedByCounter(containsTransfersToUpdate);
                            mModelMutex.unlock();
                        }
                    });
                    mUpdateTransferWatcher.setFuture(future);
                }
                else
                {
                    if(mModelMutex.tryLock())
                    {
                        processUpdateTransfers();
                        updateUiBlockedByCounter(containsTransfersToUpdate);
                        mModelMutex.unlock();
                    }
                }
            }
            else
            {
                if(isUiBlockedByCounter())
                {
                    setUiBlockedByCounterMode(false);
                }
            }
        }

        //Do not update transfers count yet, as the filtering will be done in a differente thread
        if(!asynchronousProcessed)
        {
            modelHasChanged(true);
            updateTransfersCount();
        }

        if(isUiBlockedModeActive())
        {
            setUiBlockedMode(false);
        }
    }
    else
    {
        modelHasChanged(false);

        if(isUiBlockedModeActive())
        {
            setUiBlockedMode(false);
        }
        else if(isUiBlockedByCounter())
        {
            setUiBlockedByCounterMode(false);
        }
        else
        {
            mostPriorityTransferMayChanged(false);
        }
    }
}

void TransfersModel::processStartTransfers(
    QList<QExplicitlySharedDataPointer<TransferData>>& transfersToStart)
{
    if (!transfersToStart.isEmpty())
    {
        auto pending = std::exchange(transfersToStart, {});

        // First pass to remove duplicate transfers
        // TODO: QT6 version:
        // pending.removeIf(
        //     [this](const auto& transfer)
        //     {
        //         return getRowByTransferTag(transfer->mTag) >= 0;
        //     });
        // QT5 version:
        for (auto it = pending.begin(); it != pending.end();)
        {
            if (getRowByTransferTag((*it)->mTag) >= 0)
            {
                it = pending.erase(it);
            }
            else
            {
                ++it;
            }
        }

        // Second pass to start transfers
        if (!pending.isEmpty())
        {
            const auto firstRow = rowCount(DEFAULT_IDX);
            const auto lastRow = static_cast<int>(firstRow + pending.size() - 1);

            beginInsertRows(DEFAULT_IDX, firstRow, lastRow);

            for (const auto& transfer: pending)
            {
                startTransfer(transfer);
            }

            endInsertRows();
        }
    }
}

void TransfersModel::startTransfer(QExplicitlySharedDataPointer<TransferData> transfer)
{
    addTransfer(transfer);

    auto state (transfer->getState());

    if (mAreAllPaused && (state & TransferData::PAUSABLE_STATES_MASK))
    {
        mMegaApi->pauseTransferByTag(transfer->mTag, true);
        transfer->setState(TransferData::TRANSFER_PAUSED);

        //Otherwise when filtering there will be wrong result
        transfer->setPreviousState(TransferData::TRANSFER_NONE);
    }
}

void TransfersModel::updateTransfer(QExplicitlySharedDataPointer<TransferData> transfer, int row)
{
    checkActiveTransfer(transfer->mTag, transfer->isActive());

    mDataMutex.lockForWrite();
    mTransfers[row] = transfer;
    mDataMutex.unlock();
}

void TransfersModel::processUpdateTransfers()
{
    const auto pending = std::exchange(mTransfersToProcess.updateTransfersByTag, {});

    for (const auto& transfer: std::as_const(pending))
    {
        const auto row = getRowByTransferTag(transfer->mTag);
        const auto current = getTransfer(row);

        if (current && !current->ignoreUpdate(transfer->getState()))
        {
            transfer->setPreviousState(current->getState());

            updateTransfer(transfer, row);
            sendDataChanged(row);
            transfer->resetStateHasChanged();
        }
    }
}

void TransfersModel::processFailedTransfers()
{
    const auto pending = std::exchange(mTransfersToProcess.failedTransfersByTag, {});

    for (const auto& transfer: pending)
    {
        const auto row = getRowByTransferTag(transfer->mTag);
        const auto current = getTransfer(row);

        if (current)
        {
            transfer->setPreviousState(current->getState());
            updateTransfer(transfer, row);

            if (transfer->isSyncTransfer())
            {
                mFailedTransferToClear.append(transfer->mTag);
            }
            else
            {
                sendDataChanged(row);
            }

            transfer->resetStateHasChanged();
        }
    }
}

void TransfersModel::cacheCancelTransfersTags()
{
    const auto pending = std::exchange(mTransfersToProcess.canceledTransfersByTag, {});

    mRowsToCancel.reserve(mRowsToCancel.size() + pending.size());

    for (const auto& transfer: pending)
    {
        mRowsToCancel.append(transfer->mTag);
    }
}

void TransfersModel::processCancelTransfers()
{
    if (!mRowsToCancel.isEmpty())
    {
        QModelIndexList indexesToCancel;

        foreach(auto tag, std::as_const(mRowsToCancel))
        {
            auto row = getRowByTransferTag(tag);
            if(row >= 0)
            {
                indexesToCancel.append(index(row, 0, DEFAULT_IDX));
            }

            checkActiveTransfer(tag, false);
        }

        mRowsToCancel.clear();

        const auto totalRows = rowCount();
        const auto cancelledPercentage =
            totalRows > 0 ? (indexesToCancel.size() * 100) / totalRows : 0;

        //For large amount of transfers, this is quite faster: remove all transfers and recreate the tags by row map
        if (indexesToCancel.size() >= QUICK_CANCEL_THRESHOLD ||
            (indexesToCancel.size() > QUICK_CANCEL_MIN_THRESHOLD &&
             cancelledPercentage > QUICK_CANCEL_PERCENTAGE_THRESHOLD))
        {
            std::sort(indexesToCancel.begin(), indexesToCancel.end(),[](QModelIndex check1, QModelIndex check2){
                return check1.row() > check2.row();
            });

            foreach(const auto& index, std::as_const(indexesToCancel))
            {
                removeTransfer(index.row());
            }

            restoreTagsByRow();
        }
        else
        {
            removeRows(indexesToCancel);
        }
    }
}

void TransfersModel::processSyncFailedTransfers()
{
    if(mFailedTransferToClear.size() > 0)
    {
        QModelIndexList indexesToClear;

        foreach(auto tag, mFailedTransferToClear)
        {
            auto row = getRowByTransferTag(tag);
            if(row >= 0)
            {
                indexesToClear.append(index(row,0, DEFAULT_IDX));
            }
        }

        mFailedTransferToClear.clear();

        removeRows(indexesToClear);
    }
}

void TransfersModel::getLinks(const QList<int> &rows)
{
    if (!rows.isEmpty())
    {
        QList<MegaHandle> exportList;
        QStringList linkList;

        QMutexLocker lock(&mModelMutex);

        for (auto row : rows)
        {
            auto d (getTransfer(row));

            std::unique_ptr<MegaNode> node(getNodeToOpenByRow(row));

            if (!node || !node->isPublic())
            {
                if(!exportList.contains(d->mNodeHandle))
                {
                    exportList.push_back(d->mNodeHandle);
                }
            }
            else if (node)
            {
                std::unique_ptr<char[]> handle(node->getBase64Handle());
                std::unique_ptr<char[]> key(node->getBase64Key());
                if (handle && key)
                {
                    auto link = ServiceUrls::instance()
                                    ->getRemoteNodeLinkUrl(QString::fromUtf8(handle.get()),
                                                           QString::fromUtf8(key.get()))
                                    .toString();
                    if (!linkList.contains(link))
                    {
                        linkList.push_back(link);
                    }
                }
            }
        }
        if (exportList.size() || linkList.size())
        {
            qobject_cast<MegaApplication*>(qApp)->exportNodes(exportList, linkList);
        }
    }
}

void TransfersModel::openInMEGA(const QList<int> &rows)
{
    if (!rows.isEmpty())
    {
        QMutexLocker lock(&mModelMutex);
        QSet<QUrl> urlsOpened;
        for (auto row : rows)
        {
            auto node = getParentNodeToOpenByRow(row);

            if (node)
            {
                std::unique_ptr<char[]> handle(node->getBase64Handle());
                std::unique_ptr<char[]> key(node->getBase64Key());
                if (handle && key)
                {
                    QUrl url;
                    std::unique_ptr<MegaError> err(
                        MegaSyncApp->getMegaApi()->isNodeSyncableWithError(node.get()));
                    if (err->getSyncError() == SyncError::ACTIVE_SYNC_SAME_PATH ||
                        err->getSyncError() == SyncError::ACTIVE_SYNC_BELOW_PATH ||
                        err->getSyncError() == SyncError::ACTIVE_SYNC_ABOVE_PATH)
                    {
                        auto deviceID = QString::fromUtf8(MegaSyncApp->getMegaApi()->getDeviceId());
                        url = ServiceUrls::getOpenInMegaUrl(deviceID,
                                                            QString::fromUtf8(handle.get()));
                    }
                    else
                    {
                        url = ServiceUrls::getNodeUrl(QString::fromUtf8(handle.get()));
                    }
                    if (!urlsOpened.contains(url))
                    {
                        urlsOpened << url;
                        Utilities::openUrl(url);
                    }
                }
            }
        }
    }
}

std::unique_ptr<MegaNode> TransfersModel::getNodeToOpenByRow(int row)
{
    auto d (getTransfer(row));
    return d->getNode();
}

//Returns the node if the parent node does not exist
std::unique_ptr<MegaNode> TransfersModel::getParentNodeToOpenByRow(int row)
{
    auto node = getNodeToOpenByRow(row);
    std::unique_ptr<mega::MegaNode> parentNode(mMegaApi->getParentNode(node.get()));
    if(parentNode)
    {
        return parentNode;
    }
    else
    {
        return node;
    }
}

void TransfersModel::openFolderByIndex(const QModelIndex& index)
{
    QFileInfo fileInfo = getFileInfoByIndex(index);
    openFolder(fileInfo);
}

void TransfersModel::openFoldersByIndexes(const QModelIndexList &indexes)
{
    QStringList openedFolders;

    for (auto index : indexes)
    {
        if (index.isValid())
        {
            QFileInfo fileInfo = getFileInfoByIndex(index);
            auto path(fileInfo.path());
            if(!openedFolders.contains(path))
            {
                openedFolders.append(path);
                openFolder(fileInfo);
            }
        }
    }
}

void TransfersModel::openFolder(const QFileInfo& info)
{
    if(info.exists())
    {
        QThreadPool::globalInstance()->start(
            [this, info]
            {
                emit showInFolderFinished(Platform::getInstance()->showInFolder(info.filePath()));
            });
    }
    else
    {
        MessageDialogInfo msgInfo;
        msgInfo.descriptionText = MegaTransferView::errorOpeningFileText();
        MessageDialogOpener::warning(msgInfo);
    }
}

QFileInfo TransfersModel::getFileInfoByIndex(const QModelIndex& index)
{
    QMutexLocker lock(&mModelMutex);

    const auto transferItem (
                qvariant_cast<TransferItem>(index.data(Qt::DisplayRole)));
    auto d (transferItem.getTransferData());
    auto path = d->path();
    return QFileInfo(path);
}

void TransfersModel::retryTransfers(const QMultiMap<unsigned long long, QExplicitlySharedDataPointer<TransferData>> &transfersToRetry)
{
    //This method receives a list of uploads or downloads, never mixed
    auto future = QtConcurrent::run(
        [transfersToRetry, this]()
        {
            foreach(auto& appData, transfersToRetry.uniqueKeys())
            {
                auto transferDatas = transfersToRetry.values(appData);

                QByteArray appDataBA = QString::number(appData).toUtf8();
                const char* appDataRaw = appDataBA.constData();

                foreach(auto& failedTransferdata, transferDatas)
                {
                    if (!failedTransferdata)
                    {
                        continue;
                    }
                    auto failedTransfer = failedTransferdata->mFailedTransfer;

                    std::shared_ptr<TransferMetaData> data(nullptr);

                    auto transferAppData(failedTransfer->getAppData());
                    if (transferAppData != appDataRaw)
                    {
                        auto oldAppDataId =
                            TransferMetaDataContainer::appDataToId(failedTransfer->getAppData());
                        if (oldAppDataId.first)
                        {
                            data = TransferMetaDataContainer::getAppDataById(oldAppDataId.second);
                            if (data)
                            {
                                TransferMetaDataContainer::retryTransfer(failedTransfer.get(),
                                                                         oldAppDataId.second);
                            }
                        }
                    }

                    data = TransferMetaDataContainer::getAppDataById(appData);
                    // When retrying, the appDataId is a new one
                    if (!data)
                    {
                        if (failedTransfer->getType() == mega::MegaTransfer::TYPE_UPLOAD)
                        {
                            data = TransferMetaDataContainer::createTransferMetaDataWithappDataId<
                                UploadTransferMetaData>(appData, failedTransfer->getParentHandle());
                        }
                        else
                        {
                            data = TransferMetaDataContainer::createTransferMetaDataWithappDataId<
                                DownloadTransferMetaData>(
                                appData,
                                QString::fromUtf8(failedTransfer->getParentPath()));
                        }

                        data->setInitialTransfers(transferDatas.size());
                    }
                    else
                    {
                        TransferMetaDataContainer::retryTransfer(failedTransfer.get(), appData);
                    }

                    if (failedTransfer->getType() == MegaTransfer::TYPE_DOWNLOAD)
                    {
                        std::unique_ptr<mega::MegaNode> node = failedTransferdata->getNode();
                        // If node is null, then it was intended to be undeleted
                        bool undelete = (!node);
                        // getPath()/getFileName() may be null; pass them through untouched:
                        // the SDK treats a null localPath/customName as "use the default
                        // download folder / the node's name", while an empty string bypasses
                        // that handling (and asserts in Path::normalizeAbsolute in debug).
                        mMegaApi->startDownload(node.get(),
                                                failedTransfer->getPath(),
                                                failedTransfer->getFileName(),
                                                appDataRaw,
                                                false,
                                                nullptr,
                                                MegaTransfer::COLLISION_CHECK_FINGERPRINT,
                                                MegaTransfer::COLLISION_RESOLUTION_NEW_WITH_N,
                                                undelete,
                                                nullptr);
                    }
                    else
                    {
                        std::unique_ptr<mega::MegaNode> parentNode(
                            MegaSyncApp->getMegaApi()->getNodeByHandle(
                                failedTransfer->getParentHandle()));
                        MegaUploadOptions options;
                        options.fileName = safeCharPtr(failedTransfer->getFileName());
                        options.appData = appDataRaw;
                        options.pitagTrigger = mega::MegaApi::PITAG_TRIGGER_PICKER;

                        mMegaApi->startUpload(safeCharPtr(failedTransfer->getPath()),
                                              parentNode.get(),
                                              nullptr,
                                              &options,
                                              nullptr);
                    }
                }
            }
        });

    mRetryTransferWatcher.setFuture(future);
    connect(&mRetryTransferWatcher,
            &QFutureWatcher<void>::finished,
            this,
            [this]()
            {
                updateTransfersCount();
            });
}

const char* TransfersModel::safeCharPtr(const char* value)
{
    return value ? value : "";
}

void TransfersModel::retryTransferByIndex(const QModelIndex& index)
{
    mModelMutex.lock();

    const auto transferItem(qvariant_cast<TransferItem>(index.data(Qt::DisplayRole)));
    auto d(transferItem.getTransferData());

    if (d && d->mFailedTransfer && d->canBeRetried())
    {
        QMultiMap<unsigned long long, QExplicitlySharedDataPointer<TransferData>> transfersToRetry;
        auto copiedTransfer = std::shared_ptr<mega::MegaTransfer>(d->mFailedTransfer->copy());

        unsigned long long appData(0);

        auto data = TransferMetaDataContainer::getAppData(copiedTransfer.get());
        if (data)
        {
            appData = data->getAppId();
        }
        else
        {
            if (d->isUpload())
            {
                appData = mPreferences->transferIdentifier();
            }
            else
            {
                appData = mPreferences->transferIdentifier();
            }
        }

        transfersToRetry.insert(appData, d);

        mModelMutex.unlock();

        retryTransfers(transfersToRetry);

        QModelIndexList indexToRemove;
        indexToRemove.append(index);
        clearFailedTransfers(indexToRemove);
    }
    else
    {
        mModelMutex.unlock();
    }
}

void TransfersModel::retryTransfers(QModelIndexList indexes, unsigned long long suggestedUploadAppData, unsigned long long suggestedDownloadAppData)
{
    if(indexes.size() > FAILED_THRESHOLD_THREAD)
    {
        setUiBlockedMode(true);
    }

    std::sort(indexes.begin(), indexes.end(),[](QModelIndex index1, QModelIndex index2){
        return index1.row() > index2.row();
    });

    QMultiMap<unsigned long long, QExplicitlySharedDataPointer<TransferData>> uploadTransfersToRetry;
    QMultiMap<unsigned long long, QExplicitlySharedDataPointer<TransferData>> downloadTransfersToRetry;
    QModelIndexList canBeRetriedIndexes;

    mModelMutex.lock();

    unsigned long long newAppDataIdUpload(suggestedUploadAppData);
    unsigned long long newAppDataIdDownload(suggestedDownloadAppData);

    foreach(auto index, indexes)
    {
        const auto transferItem (
                    qvariant_cast<TransferItem>(index.data(Qt::DisplayRole)));
        auto d (transferItem.getTransferData());

        if(d && d->isFailed() && d->canBeRetried())
        {
            canBeRetriedIndexes.append(index);

            auto copiedTransfer = std::shared_ptr<mega::MegaTransfer>(d->mFailedTransfer->copy());

            if(d->isUpload())
            {
                unsigned long long appDataId(newAppDataIdUpload);
                if(appDataId == 0)
                {
                    auto appData = TransferMetaDataContainer::getAppData(copiedTransfer.get());
                    if(appData)
                    {
                        appDataId = appData->getAppId();
                    }
                    else
                    {
                        if(newAppDataIdUpload == 0)
                        {
                            newAppDataIdUpload = mPreferences->transferIdentifier();
                        }

                        appDataId = newAppDataIdUpload;
                    }
                }

                uploadTransfersToRetry.insert(appDataId, d);
            }
            else
            {
                unsigned long long appDataId(newAppDataIdDownload);

                if(appDataId == 0)
                {
                    auto appData = TransferMetaDataContainer::getAppData(copiedTransfer.get());
                    if(appData)
                    {
                        appDataId = appData->getAppId();
                    }
                    else
                    {
                        if(newAppDataIdUpload == 0)
                        {
                            newAppDataIdDownload = mPreferences->transferIdentifier();
                        }

                        appDataId = newAppDataIdDownload;
                    }
                }

                downloadTransfersToRetry.insert(appDataId, d);
            }
        }
    }

    mModelMutex.unlock();

    if(!uploadTransfersToRetry.isEmpty())
    {
        retryTransfers(uploadTransfersToRetry);
    }

    if(!downloadTransfersToRetry.isEmpty())
    {
        retryTransfers(downloadTransfersToRetry);
    }

    clearFailedTransfers(canBeRetriedIndexes);
}

void TransfersModel::retryTransfersByAppDataId(const std::shared_ptr<TransferMetaData>& data)
{
    QModelIndexList fileIndexesToRetry;

    QList<std::shared_ptr<TransferMetaDataItem>> filesToRetry;
    QList<TransferMetaDataItemId> foldersToRetry;

    data->getFileTransferFailedTags(filesToRetry, foldersToRetry);

    QMultiMap<unsigned long long, QExplicitlySharedDataPointer<TransferData>> failedFilesToRetryOutOfTheModel;

    foreach(auto item, std::as_const(filesToRetry))
    {
        auto itemIndex = index(getRowByTransferTag(item->id.tag),0);
        if(itemIndex.isValid())
        {
            fileIndexesToRetry.append(itemIndex);
        }
        else
        {
           failedFilesToRetryOutOfTheModel.insert(data->getAppId(), getTransferByTag(item->failedTransfer->getTag()));
        }
    }

    //For transfers still in the model
    //All transfers have the same appdataid as all are of the same type (download or upload)
    retryTransfers(fileIndexesToRetry, data->getAppId(), data->getAppId());

    //For transfers removed from the model
    retryTransfers(failedFilesToRetryOutOfTheModel);
}

void TransfersModel::openFolderByTag(TransferTag tag)
{
    auto indexToOpen = index(getRowByTransferTag(tag), 0);
    if(indexToOpen.isValid())
    {
        openFolderByIndex(indexToOpen);
    }
}

uint TransfersModel::failedTransfers()
{
    return mTransfersCount.totalFailedTransfers();
}

void TransfersModel::cancelAllTransfers(QWidget* canceledFrom)
{
    auto count = rowCount(DEFAULT_IDX);

    mModelMutex.lock();
    for (auto row = 0; row < count;++row)
    {
        auto d (getTransfer(row));

        // Clear (remove rows of) finished transfers
        if (d && d->isSyncTransfer() && !d->isFinished())
        {
            if(!mSyncsInRowsToCancel)
            {
                mSyncsInRowsToCancel = true;
                mCancelledFrom = canceledFrom;
                break;
            }
        }
    }
    mModelMutex.unlock();

    //Cancel little by little??? CAnceling everythin blocks the SDK
    mMegaApi->cancelTransfers(MegaTransfer::TYPE_UPLOAD);
    mMegaApi->cancelTransfers(MegaTransfer::TYPE_DOWNLOAD);
}

void TransfersModel::cancelAndClearTransfers(const QModelIndexList& indexes, QWidget* canceledFrom)
{
    if(indexes.isEmpty())
    {
        return;
    }

    QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>> uploadToClear;
    QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>> downloadToClear;

    QList<TransferTag> toCancel;

    // First clear finished transfers (remove rows), then cancel the others.
    // This way, there is no risk of messing up the rows order with cancel requests.
    mModelMutex.lock();
    for (auto index : indexes)
    {
        auto d (getTransfer(index.row()));

        // Clear (remove rows of) finished transfers
        if (d)
        {
            if(!d->isCancelable())
            {
                if(d->isFinished())
                {
                    if(d->isFailed())
                    {
                        classifyUploadOrDownloadFailedTransfers(uploadToClear, downloadToClear,index);
                    }
                    else
                    {
                        classifyUploadOrDownloadCompletedTransfers(uploadToClear, downloadToClear,index);
                    }
                }
                else
                {
                    if(d->isSyncTransfer())
                    {
                        mSyncsInRowsToCancel = true;
                        mCancelledFrom = canceledFrom;
                    }
                }
            }
            else
            {
                toCancel.append(d->mTag);
            }
        }
    }
    mModelMutex.unlock();

    if(!uploadToClear.isEmpty() || !downloadToClear.isEmpty())
    {
        clearTransfers(uploadToClear, downloadToClear);
    }

    if(!toCancel.isEmpty())
    {
        auto counter(0);
        // Now cancel transfers
        for (auto item : toCancel)
        {
            mMegaApi->cancelTransferByTag(item);

            //This is done to avoid GUI freezes
            if(++counter == 100)
            {
                counter = 0;
                MegaSyncApp->processEvents();
            }
        }
    }
}

void TransfersModel::showSyncCancelledWarning()
{
    if(syncsInRowsToCancel())
    {
        MessageDialogInfo msgInfo;
        msgInfo.descriptionText = tr("Sync transfers cannot be cancelled individually.\n"
                                     "Please delete the folder sync from settings to cancel them.");
        msgInfo.parent = mCancelledFrom;
        msgInfo.buttons = QMessageBox::Yes|QMessageBox::No;
        QMap<QMessageBox::Button, QString> textsByButton;
        textsByButton.insert(QMessageBox::Yes, tr("Open settings"));
        textsByButton.insert(QMessageBox::No, tr("Dismiss"));
        msgInfo.buttonsText = textsByButton;
        msgInfo.defaultButton = QMessageBox::Yes;
        msgInfo.finishFunc = [this](QPointer<MessageDialogResult> msg)
        {
            if(msg->result() == QMessageBox::Yes)
            {
                //Do it in the following event loop to ensure dialog is correctly raised
                QTimer::singleShot(0,
                                   []()
                                   {
                                       MegaSyncApp->openSettings(SettingsDialog::SYNCS_TAB);
                                   });
            }

            resetSyncInRowsToCancel();
        };
        MessageDialogOpener::warning(msgInfo);
    }
}

bool TransfersModel::syncsInRowsToCancel() const
{
    return mSyncsInRowsToCancel;
}

void TransfersModel::resetSyncInRowsToCancel()
{
    mSyncsInRowsToCancel = false;
    mCancelledFrom = nullptr;
}

void TransfersModel::classifyUploadOrDownloadCompletedTransfers(QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>>& uploads,
                                                       QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>>& downloads,
                                                       const QModelIndex& index)
{
    auto d (getTransfer(index.row()));

    // Clear (remove rows of) finished transfers
    if (d && d->isCompleted())
    {
        if(d->isUpload())
        {
            uploads.insert(index, d);
        }
        else
        {
            downloads.insert(index, d);
        }
    }
}

void TransfersModel::classifyUploadOrDownloadFailedTransfers(QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>>& uploads,
                                                       QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>>& downloads,
                                                             const QModelIndex& index)
{
    auto d (getTransfer(index.row()));

    // Clear (remove rows of) finished transfers
    if (d && d->isFailed())
    {
        if(d->isUpload())
        {
            uploads.insert(index, d);
        }
        else
        {
            downloads.insert(index, d);
        }
    }
}

void TransfersModel::clearAllTransfers()
{
    QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>> uploadToClear;
    QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>> downloadToClear;

    auto totalRows(rowCount(DEFAULT_IDX));

    EventUpdater updater(totalRows, 500);

    for (auto row = 0; row < totalRows; ++row)
    {
        auto indexToCheck = index(row, 0);
        classifyUploadOrDownloadCompletedTransfers(uploadToClear, downloadToClear,indexToCheck);

        updater.update(row);
    }

    clearTransfers(uploadToClear, downloadToClear);
}

void TransfersModel::clearTransfers(const QModelIndexList& indexes)
{   
    if(indexes.isEmpty())
    {
        return;
    }

    QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>> uploadToClear;
    QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>> downloadToClear;

    for (auto indexToCheck : indexes)
    {
        classifyUploadOrDownloadCompletedTransfers(uploadToClear, downloadToClear,indexToCheck);
    }

    clearTransfers(uploadToClear, downloadToClear);
}

void TransfersModel::clearFailedTransfers(const QModelIndexList &indexes)
{
    if(indexes.isEmpty())
    {
        return;
    }

    QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>> uploadToClear;
    QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>> downloadToClear;

    for (auto indexToCheck : indexes)
    {
        classifyUploadOrDownloadFailedTransfers(uploadToClear, downloadToClear,indexToCheck);
    }

    clearTransfers(uploadToClear, downloadToClear);
}

void TransfersModel::clearTransfers(const QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData> > &uploads,
                                    const QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData> > &downloads)
{
    if(!uploads.isEmpty() || !downloads.isEmpty())
    {
        auto totalTransfersToClear(uploads.size() + downloads.size());
        if(totalTransfersToClear > CLEAR_THRESHOLD_THREAD)
        {
            setUiBlockedMode(true);
            pauseModelProcessing(true);

            auto future = QtConcurrent::run([this, uploads, downloads]()
            {
                blockModelSignals(true);
                performClearTransfers(uploads, downloads);
                blockModelSignals(false);
            });
            mClearTransferWatcher.setFuture(future);
        }
        else
        {
            performClearTransfers(uploads, downloads);
            updateTransfersCount();
        }
    }
}

void TransfersModel::onClearTransfersFinished()
{
    updateTransfersCount();
    pauseModelProcessing(false);

    //The clear transfer is the only action which does not receive a SDK request
    emit transfersProcessChanged();
    emit unblockUiAndFilter();
}

void TransfersModel::onUpdateTransfersFinished()
{
    modelHasChanged(true);
    updateTransfersCount();
}

void TransfersModel::onKeepPCAwake()
{
    PowerOptions options;
    options.keepAwake(hasActiveTransfers());
}

void TransfersModel::performClearTransfers(const QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>>& uploads,
                                           const QMap<QModelIndex, QExplicitlySharedDataPointer<TransferData>>& downloads)
{
    QModelIndexList itemsToRemove;

    if(!uploads.isEmpty())
    {
        mTransferEventWorker->resetUploads(uploads.values());

        itemsToRemove.append(uploads.keys());
    }

    if(!downloads.isEmpty())
    {
        mTransferEventWorker->resetDownloads(downloads.values());

        itemsToRemove.append(downloads.keys());
    }

    //About to remove transfers, be careful with the other threads
    mModelMutex.lock();
    removeRows(itemsToRemove);
    mModelMutex.unlock();

    //The clear transfer is the only action which does not receive a SDK request
    emit transfersProcessChanged();
}

void TransfersModel::pauseTransfers(const QModelIndexList& indexes, bool pauseState)
{
    if(!indexes.isEmpty())
    {
        setUiBlockedModeByCounter(indexes.size());

        if(indexes.size() > PAUSE_RESUME_THRESHOLD_THREAD)
        {
            QThreadPool::globalInstance()->start(
                [this, indexes, pauseState]()
                {
                    blockModelSignals(true);
                    performPauseResumeVisibleTransfers(indexes, pauseState, false);
                    blockModelSignals(false);

                    emit pauseStateChanged(mAreAllPaused);
                });
        }
        else
        {
            performPauseResumeVisibleTransfers(indexes, pauseState, true);
        }
    }
}

void TransfersModel::performPauseResumeVisibleTransfers(const QModelIndexList& indexes,
                                                        bool pauseState,
                                                        bool useEventUpdater)
{
    EventUpdater updater(indexes.size(), 1000);
    auto tagsUpdated(0);

    QMutexLocker lock(&mModelMutex);

    for (auto index : indexes)
    {
        auto d = getTransfer(index.row());
        // In some cases, when we pause/resume in another thread, some indexes may be outdated
        if (d)
        {
            if ((pauseState && d->getState() & TransferData::PAUSABLE_STATES_MASK) ||
                (!pauseState && d->getState() & TransferData::TRANSFER_PAUSED))
            {
                pauseResumeTransferByTag(d->mTag, pauseState);
            }
        }

        if(useEventUpdater)
        {
            tagsUpdated++;
            updater.update(tagsUpdated);
        }
    }
}

void TransfersModel::blockModelSignals(bool state)
{
    blockSignals(state);
}

void TransfersModel::pauseResumeAllTransfers(bool state)
{
    mAreAllPaused = state;
    auto activeTransfers = rowCount();

    //Provisional active transfers, just used to know if UI will be blocked
    //The final count can be +- 30 transfers
    if(activeTransfers > PAUSE_RESUME_THRESHOLD_THREAD)
    {
        QThreadPool::globalInstance()->start(
            [this, activeTransfers]()
            {
                blockModelSignals(true);
                auto tagsUpdated = performPauseResumeAllTransfers(activeTransfers, false);
                blockModelSignals(false);

                setUiBlockedModeByCounter(tagsUpdated);
            });
    }
    else
    {
        auto tagsUpdated = performPauseResumeAllTransfers(activeTransfers, true);
        setUiBlockedModeByCounter(tagsUpdated);
    }
}

int TransfersModel::performPauseResumeAllTransfers(int activeTransfers, bool useEventUpdater)
{
    auto tagsUpdated(0);

    QMutexLocker lock(&mModelMutex);

    auto transfers = getTransfersToIterate();

    if (mAreAllPaused)
    {
        //This needs to be done before retrying all the transfers one by one
        mMegaApi->pauseTransfers(mAreAllPaused);

        EventUpdater updater(activeTransfers, 200);

        std::for_each(transfers.crbegin(), transfers.crend(), [this, &tagsUpdated, updater, useEventUpdater](QExplicitlySharedDataPointer<TransferData> item)
                      mutable {

            if(item->getState() & TransferData::PAUSABLE_STATES_MASK)
            {
                pauseResumeTransferByTag(item->mTag, mAreAllPaused);
                tagsUpdated++;
            }

            if(useEventUpdater)
            {
                updater.update(tagsUpdated);
            }
        });
    }
    else
    {
        EventUpdater updater(activeTransfers, 200);

        std::for_each(transfers.cbegin(), transfers.cend(), [this, &tagsUpdated, updater, useEventUpdater](QExplicitlySharedDataPointer<TransferData> item)
                      mutable {

            if(item->getState() & TransferData::TRANSFER_PAUSED)
            {
                pauseResumeTransferByTag(item->mTag, mAreAllPaused);
                tagsUpdated++;
            }

            if(useEventUpdater)
            {
                updater.update(tagsUpdated);
            }
        });

        //This needs to be done after pausing all the transfers one by one
        mMegaApi->pauseTransfers(mAreAllPaused);
    }

    return tagsUpdated;
}

void TransfersModel::pauseResumeTransferByTag(TransferTag tag, bool pauseState)
{
    auto row(getRowByTransferTag(tag));
    auto d  = getTransfer(row);

    if(d)
    {
        if(!pauseState && mAreAllPaused)
        {
            mMegaApi->pauseTransfers(pauseState);
            mAreAllPaused = false;
        }

        if(pauseState)
        {
            if(d->getState() & TransferData::PAUSABLE_STATES_MASK)
            {
                d->setPauseResume(true);
            }
        }
        else
        {
            d->setPauseResume(false);
        }

        sendDataChanged(row);
        d->resetStateHasChanged();
        mMegaApi->pauseTransferByTag(d->mTag, pauseState);
    }
}

void TransfersModel::pauseResumeTransferByIndex(const QModelIndex &index, bool pauseState)
{
    QMutexLocker lock(&mModelMutex);

    const auto transferItem (
                qvariant_cast<TransferItem>(index.data(Qt::DisplayRole)));

    pauseResumeTransferByTag(transferItem.getTransferData()->mTag, pauseState);
}

void TransfersModel::globalPauseStateChanged(bool state)
{
    mAreAllPaused = state;
    emit pauseStateChanged(state);
}

void TransfersModel::setGlobalPause(bool state)
{
    mAreAllPaused = state;
    mMegaApi->pauseTransfers(state);
}

void TransfersModel::lockModelMutex(bool lock)
{
    if (lock)
    {
        mModelMutex.lock();
    }
    else
    {
        mModelMutex.unlock();
    }
}

uint TransfersModel::getNumberOfTransfersForFileType(Utilities::FileType fileType) const
{
    return mTransfersCount.transfersByType.value(fileType);
}

uint TransfersModel::getNumberOfFinishedForFileType(Utilities::FileType fileType) const
{
    return mTransfersCount.transfersFinishedByType.value(fileType);
}

TransfersCount TransfersModel::getTransfersCount()
{
    return mTransfersCount;
}

TransfersCount TransfersModel::getLastTransfersCount()
{
    return mLastTransfersCount;
}

void TransfersModel::updateTransfersCount()
{    
    mTransfersCount = mTransferEventWorker->getTransfersCount();
    mLastTransfersCount = mTransferEventWorker->getLastTransfersCount();

    emit transfersCountUpdated();
}

void TransfersModel::removeRows(QModelIndexList& indexesToRemove)
{
    if(indexesToRemove.isEmpty())
    {
        return;
    }

    std::sort(indexesToRemove.begin(), indexesToRemove.end(),[](QModelIndex check1, QModelIndex check2){
        return check1.row() > check2.row();
    });

    // First clear finished transfers (remove rows), then cancel the others.
    // This way, there is no risk of messing up the rows order with cancel requests.
    int count (0);
    //We add 1 to row, as the row will be reduced by one as soon as the loop starts
    int row (indexesToRemove.last().row() + 1);
    for (auto index : indexesToRemove)
    {
        row--;

        // Init row with row of first tag
        if (count == 0)
        {
            row = index.row();
        }

        // If rows are non-contiguous, flush and start from item
        if (row != index.row())
        {
            removeRows(row + 1, count, DEFAULT_IDX);

            count = 0;
            row = index.row();
        }

        // We have at least one row
        count++;
    }
    // Flush pooled rows (start at row).
    // This happens when the last item processed is in a finished state.
    if (count > 0 && row >= 0)
    {
        removeRows(row, count, DEFAULT_IDX);
    }
}

QExplicitlySharedDataPointer<TransferData> TransfersModel::getTransfer(int row) const
{
    QExplicitlySharedDataPointer<TransferData> transfer(nullptr);

    mDataMutex.lockForRead();
    if(row >= 0 && mTransfers.size() > row)
    {
        transfer = mTransfers.at(row);
    }
    mDataMutex.unlock();

    return transfer;
}

const QExplicitlySharedDataPointer<const TransferData> TransfersModel::getTransferByTag(int tag) const
{
    return getTransfer(getRowByTransferTag(tag));
}

QExplicitlySharedDataPointer<TransferData> TransfersModel::getTransferByTag(int tag)
{
    return getTransfer(getRowByTransferTag(tag));
}

int TransfersModel::getRowByTransferTag(int tag) const
{
    auto result = mTagByOrder.contains(tag) ? mTagByOrder.value(tag).row() : -1;
    return result;
}

void TransfersModel::addTransfer(QExplicitlySharedDataPointer<TransferData> transfer)
{
    mDataMutex.lockForWrite();
    mTransfers.append(transfer);
    mDataMutex.unlock();

    mTagByOrder.insert(transfer->mTag, QPersistentModelIndex(index(rowCount(DEFAULT_IDX) - 1,0)));
}

void TransfersModel::removeTransfer(int row)
{
    mDataMutex.lockForWrite();
    if(row >= 0  && row < mTransfers.size())
    {
        auto transfer = mTransfers.takeAt(row);
        mTagByOrder.remove(transfer->mTag);
    }
    mDataMutex.unlock();
}

void TransfersModel::sendDataChangedByTag(int tag)
{
    sendDataChanged(getRowByTransferTag(tag));
}

void TransfersModel::sendDataChanged(int row)
{
    if(!signalsBlocked())
    {
        QModelIndex indexChanged (index(row, 0, DEFAULT_IDX));
        if(indexChanged.isValid())
        {
            emit dataChanged(indexChanged, indexChanged);
        }
    }
}

void TransfersModel::restoreTagsByRow()
{
    mTagByOrder.clear();
    for(int row = 0; row < rowCount(); ++row)
    {
        auto transfer = getTransfer(row);
        if(transfer)
        {
            mTagByOrder.insert(transfer->mTag, QPersistentModelIndex(index(row,0)));
        }
    }
}

QList<QExplicitlySharedDataPointer<TransferData> > TransfersModel::getTransfersToIterate() const
{
    mDataMutex.lockForRead();
    auto transfers = mTransfers;
    mDataMutex.unlock();
    return transfers;
}

bool TransfersModel::isUiBlockedModeActive() const
{
    return mUiBlockedCounter > 0;
}

void TransfersModel::setUiBlockedMode(bool state)
{
    if(state)
    {
        emit blockUi();

        mUiBlockedCounter = RESET_AFTER_EMPTY_RECEIVES;
    }
    else if(!state && mUiBlockedCounter != 0)
    {
        mUiBlockedCounter--;

        if(mUiBlockedCounter == 0)
        {
            if(!mRowsToCancel.isEmpty() || !mFailedTransferToClear.isEmpty())
            {
                auto task = QtConcurrent::run([this]()
                {
                    if(mModelMutex.tryLock())
                    {
                        blockModelSignals(true);
                        processCancelTransfers();
                        processSyncFailedTransfers();
                        blockModelSignals(false);

                        mModelMutex.unlock();
                    }

                    emit unblockUiAndFilter();
                });
                mUpdateTransferWatcher.setFuture(task);
            }
            else
            {
                updateTransfersCount();
                emit unblockUiAndFilter();
            }
        }
    }
}

void TransfersModel::setUiBlockedModeByCounter(qsizetype transferCount)
{
    if(transferCount > 0 && transferCount > PAUSE_RESUME_THRESHOLD_THREAD)
    {
        emit blockUi();
        setUiBlockedByCounterMode(true);
        mUiBlockedByCounter = transferCount;
        mTransferEventWorker->setMaxTransfersToProcess(20000);
    }
    else if(transferCount == 0)
    {
        emit unblockUi();
    }
}

void TransfersModel::updateUiBlockedByCounter(qsizetype updates)
{
    if(updates > 0 && mUiBlockedByCounter > 0)
    {
        if(mUiBlockedByCounter >= updates)
        {
            mUiBlockedByCounter -= updates;
        }
        else
        {
            mUiBlockedByCounter = 0;
        }

        if(mUiBlockedByCounter == 0)
        {
            mTransferEventWorker->setMaxTransfersToProcess(MAX_TRANSFERS);
            emit unblockUiAndFilter();
        }
    }
}

bool TransfersModel::isUiBlockedByCounter() const
{
    return mUiBlockedByCounter > 0;
}

void TransfersModel::setUiBlockedByCounterMode(bool state)
{
    if(state)
    {
        mUiBlockedByCounterSafety = RESET_AFTER_EMPTY_RECEIVES;
    }
    else if(!state && mUiBlockedByCounterSafety != 0)
    {
        mUiBlockedByCounterSafety--;

        if(mUiBlockedByCounterSafety == 0 && mUiBlockedByCounter > 0)
        {
            updateUiBlockedByCounter(mUiBlockedByCounter);
        }
    }
}

void TransfersModel::modelHasChanged(bool state)
{
    if(state)
    {
        if(mTransfersProcessChanged == 0)
        {
            emit transfersProcessChanged();
        }

        mTransfersProcessChanged = MODEL_HAS_CHANGED_AFTER_EMPTY_RECEIVES;
    }
    else
    {
        if(mTransfersProcessChanged > 0)
        {
            mTransfersProcessChanged--;

            if(mTransfersProcessChanged == 0)
            {
                emit transfersProcessChanged();
            }
        }
    }
}

void TransfersModel::mostPriorityTransferMayChanged(bool state)
{
    if(!state && mUpdateMostPriorityTransfer)
    {
        mUpdateMostPriorityTransfer--;
        if(mUpdateMostPriorityTransfer == 0)
        {
            mMostPriorityTransferTimer.stop();
            mMostPriorityTransferTimer.start();
        }
    }
    else if(state)
    {
        mUpdateMostPriorityTransfer = RESET_AFTER_EMPTY_RECEIVES;
        mMostPriorityTransferTimer.stop();
    }
}

void TransfersModel::askForMostPriorityTransfer()
{
    auto task = QtConcurrent::run([]()
    {
        std::unique_ptr<MegaTransfer> nextUTransfer(MegaSyncApp->getMegaApi()->getFirstTransfer(MegaTransfer::TYPE_UPLOAD));
        auto UTag = nextUTransfer ? nextUTransfer->getTag() : -1;
        std::unique_ptr<MegaTransfer> nextDTransfer(MegaSyncApp->getMegaApi()->getFirstTransfer(MegaTransfer::TYPE_DOWNLOAD));
        auto DTag = nextDTransfer ? nextDTransfer->getTag() : -1;

        return qMakePair(UTag, DTag);
    });

    mAskForMostPriorityTransfersWatcher.setFuture(task);
}

void TransfersModel::onAskForMostPriorityTransfersFinished()
{
    auto tags = mAskForMostPriorityTransfersWatcher.result();
    emit mostPriorityTransferUpdate(tags.first, tags.second);
}

bool TransfersModel::removeRows(int row, int count, const QModelIndex& parent)
{
    if (parent == DEFAULT_IDX && count > 0 && row >= 0)
    {
        beginRemoveRows(DEFAULT_IDX, row, row + count - 1);

        for (auto i (0); i < count; ++i)
        {
            removeTransfer(row);
        }

        endRemoveRows();

        return true;
    }
    else
    {
        return false;
    }
}

bool TransfersModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild)
{
    return QAbstractItemModel::moveRows(sourceParent, sourceRow, count, destinationParent, destinationChild);
}

void TransfersModel::ignoreMoveRowsSignal(bool state)
{
    mIgnoreMoveSignal = state;
}

bool TransfersModel::moveTransferPriorityByDrag(const QModelIndex& sourceParent,
                                                const QList<int>& rows,
                                                const QModelIndex& destinationParent,
                                                int destinationChild)
{
    bool result(false);

    if(!rows.isEmpty())
    {
        foreach(auto sourceRow, rows)
        {
            //TODO MOVE TO TOP THE SECOND ITEM
            int lastRow (sourceRow);

            if (sourceParent == destinationParent &&
                (destinationChild < sourceRow || destinationChild > lastRow))
            {
                // To keep order, do from first to last if destination is before first,
                // and from last to first if destination is after last.
                bool ascending (destinationChild < sourceRow ? false : true);

                QList<TransferTag> tagsToMove;

                QMutexLocker lock(&mModelMutex);

                for (auto row (sourceRow); row <= lastRow; ++row)
                {
                    auto tag(getTransfer(row)->mTag);

                    if(tag)
                    {
                        if (ascending)
                        {
                            tagsToMove.push_back(tag);
                        }
                        else
                        {
                            tagsToMove.push_front(tag);
                        }
                    }
                }

                for (auto tag : tagsToMove)
                {
                    if(destinationChild == -1)
                    {
                        mMegaApi->moveTransferToFirstByTag(tag);
                    }
                    else if (destinationChild == -2)
                    {
                        mMegaApi->moveTransferToLastByTag(tag);
                    }
                    else
                    {
                        // Get target
                        auto target (getTransfer(destinationChild));

                        if(target)
                        {
                            mMegaApi->moveTransferBeforeByTag(tag, target->mTag);
                        }
                    }

                    if (!mIgnoreMoveSignal)
                    {
                        emit rowsAboutToBeMoved(tag);
                    }
                }

                result = true;
            }
        }
    }

    return result;
}

void TransfersModel::moveTransferPriorityUp(const QModelIndexList& sourceIndexes)
{
    moveTransferPriority(sourceIndexes,
                         true,
                         [this](QExplicitlySharedDataPointer<TransferData> transfer,
                                mega::MegaRequestListener* listener)
                         {
                             mMegaApi->moveTransferUpByTag(transfer->mTag, listener);
                         });
}

void TransfersModel::moveTransferPriorityDown(const QModelIndexList& sourceIndexes)
{
    moveTransferPriority(sourceIndexes,
                         false,
                         [this](QExplicitlySharedDataPointer<TransferData> transfer,
                                mega::MegaRequestListener* listener)
                         {
                             mMegaApi->moveTransferDownByTag(transfer->mTag, listener);
                         });
}

void TransfersModel::moveTransferPriorityToTop(const QModelIndexList& sourceIndexes)
{
    moveTransferPriority(sourceIndexes,
                         false,
                         [this](QExplicitlySharedDataPointer<TransferData> transfer,
                                mega::MegaRequestListener* listener)
                         {
                             mMegaApi->moveTransferToFirstByTag(transfer->mTag, listener);
                         });
}

void TransfersModel::moveTransferPriorityToBottom(const QModelIndexList& sourceIndexes)
{
    moveTransferPriority(sourceIndexes,
                         true,
                         [this](QExplicitlySharedDataPointer<TransferData> transfer,
                                mega::MegaRequestListener* listener)
                         {
                             mMegaApi->moveTransferToLastByTag(transfer->mTag, listener);
                         });
}

void TransfersModel::moveTransferPriority(
    const QModelIndexList& sourceIndexes,
    bool processByAscPrio,
    const std::function<void(QExplicitlySharedDataPointer<TransferData>,
                             mega::MegaRequestListener*)>& func)
{
    ThreadPoolSingleton::getInstance()->push(
        [this, sourceIndexes, processByAscPrio, func]()
        {
            auto auxList(sourceIndexes);
            std::sort(auxList.begin(),
                      auxList.end(),
                      [this, processByAscPrio](QModelIndex check1, QModelIndex check2)
                      {
                          auto transfer1(getTransfer(check1.row()));
                          auto transfer2(getTransfer(check2.row()));
                          return processByAscPrio ? transfer1->mPriority < transfer2->mPriority :
                                                    transfer1->mPriority > transfer2->mPriority;
                      });

            for (const auto index: auxList)
            {
                auto transfer(getTransfer(index.row()));
                MegaApiSynchronizedRequest::runRequestLambda(func, mMegaApi, transfer);

                if (!mIgnoreMoveSignal)
                {
                    emit rowsAboutToBeMoved(transfer->mTag);
                }
            }
        });
}

void TransfersModel::resetModel()
{
    QMutexLocker lock(&mModelMutex);

    beginResetModel();

    mTransfersCount.clear();
    mActiveTransfers.clear();
    mTransferEventWorker->clear();
    mTransfersToProcess.clear();
    mTransfersProcessChanged = 0;
    mUpdateMostPriorityTransfer = 0;
    mUiBlockedCounter = 0;

    mDataMutex.lockForWrite();
    mTransfers.clear();
    mTagByOrder.clear();
    mDataMutex.unlock();

    endResetModel();
}

Qt::ItemFlags TransfersModel::flags(const QModelIndex& index) const
{
    if (index.isValid())
    {
        auto indexData = getTransfer(index.row());
        if(indexData)
        {
            if(indexData->isFinished())
            {
                return QAbstractItemModel::flags(index);
            }
            else
            {
                return QAbstractItemModel::flags(index) | Qt::ItemIsDragEnabled;
            }
        }
    }
    return QAbstractItemModel::flags(index) | Qt::ItemIsDropEnabled;
}

Qt::DropActions TransfersModel::supportedDropActions() const
{
    return Qt::MoveAction;
}

QMimeData* TransfersModel::mimeData(const QModelIndexList& indexes) const
{
    QByteArray byteArray;
    QDataStream stream (&byteArray, QIODevice::WriteOnly);
    QList<TransferTag> tags;

    mModelMutex.lock();

    for (auto index : indexes)
    {
        auto transfer = getTransfer(index.row());
        tags.push_back(static_cast<TransferTag>(transfer->mTag));
    }

    mModelMutex.unlock();

    stream << tags;

    QMimeData* data = new QMimeData();
    data->setData(QString::fromUtf8("application/x-qabstractitemmodeldatalist"), byteArray);

    emit internalMoveStarted();

    connect(data, &QMimeData::destroyed, this, [this](){
        emit internalMoveFinished();
    });

    return data;
}

bool TransfersModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int destRow,
                                  int column, const QModelIndex& parent)
{
    Q_UNUSED(column)

    if (destRow >= 0 && destRow <= rowCount(DEFAULT_IDX) && action == Qt::MoveAction)
    {
        QList<int> rows = getDragAndDropRows(data);

        moveTransferPriorityByDrag(parent, rows, parent, destRow);
    }

    // Return false to avoid row deletion...dirty!
    return false;
}

QList<int> TransfersModel::getDragAndDropRows(const QMimeData *data)
{
    QByteArray byteArray (data->data(QString::fromUtf8("application/x-qabstractitemmodeldatalist")));
    QDataStream stream (&byteArray, QIODevice::ReadOnly);
    QList<TransferTag> tags;
    stream >> tags;

    QList<int> rows;
    for (auto tag: std::as_const(tags))
    {
        auto row(getRowByTransferTag(tag));
        if(row >= 0)
        {
            rows.push_back(row);
        }
    }

    return rows;
}
