/**
 * @file heartbeats.cpp
 * @brief Classes for heartbeating Sync configuration and status
 *
 * (c) 2013 by Mega Limited, Auckland, New Zealand
 *
 * This file is part of the MEGA SDK - Client Access Engine.
 *
 * Applications using the MEGA API must present a valid application key
 * and comply with the the rules set forth in the Terms of Service.
 *
 * The MEGA SDK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @copyright Simplified (2-clause) BSD License.
 *
 * You should have received a copy of the license along with this
 * program.
 */

#include "mega/heartbeats.h"
#include "mega/command.h"
#include "assert.h"
#include "mega.h"

namespace mega {

#ifdef ENABLE_SYNC

static constexpr int FREQUENCY_HEARTBEAT_DS = 300;

HeartBeatBackupInfo::HeartBeatBackupInfo()
{
}

m_time_t HeartBeatBackupInfo::lastAction() const
{
    return mLastAction;
}

handle HeartBeatBackupInfo::lastItemUpdated() const
{
    return mLastItemUpdated;
}

void HeartBeatBackupInfo::setLastSyncedItem(const handle &lastSyncedItem)
{
    if (mLastItemUpdated != lastSyncedItem)
    {
        mLastItemUpdated = lastSyncedItem;
        updateLastActionTime();
    }
}

void HeartBeatBackupInfo::setLastAction(const m_time_t &lastAction)
{
    mLastAction = lastAction;
}

void HeartBeatBackupInfo::updateLastActionTime()
{
    setLastAction(m_time(nullptr));
    mModified = true;
}

void HeartBeatBackupInfo::setLastBeat(const m_time_t &lastBeat)
{
    mLastBeat = lastBeat;
    mModified = false;
}

m_time_t HeartBeatBackupInfo::lastBeat() const
{
    return mLastBeat;
}

void HeartBeatSyncInfo::updateSPHBStatus(UnifiedSync& us)
{
    SPHBStatus status = CommandBackupPutHeartBeat::INACTIVE;

    if (us.mSync)
    {
        switch(us.mSync->localroot->ts)
        {
        case TREESTATE_SYNCED:
            status = CommandBackupPutHeartBeat::UPTODATE;
            break;
        case TREESTATE_PENDING:
            status = CommandBackupPutHeartBeat::PENDING;
            break;
        case TREESTATE_SYNCING:
            status = CommandBackupPutHeartBeat::SYNCING;
            break;
        default:
            status = CommandBackupPutHeartBeat::UNKNOWN;
            break;
        }
    }

    if (mSPHBStatus != status)
    {
        mSPHBStatus = status;
        updateLastActionTime();
    }
}

BackupInfoSync::BackupInfoSync(const SyncConfig& config, const string& device, handle drive, CommandBackupPut::SPState calculatedState)
{
    backupId = config.mBackupId;
    type = getSyncType(config);
    backupName = config.mName,
    nodeHandle = config.mRemoteNode;
    localFolder = config.getLocalPath();
    state = calculatedState;
    subState = config.mError;
    deviceId = device;
    driveId = drive;
}

BackupInfoSync::BackupInfoSync(const UnifiedSync &us, bool pauseDown, bool pauseUp)
{
    backupId = us.mConfig.mBackupId;
    type = getSyncType(us.mConfig);
    backupName = us.mConfig.mName,
    nodeHandle = us.mConfig.mRemoteNode;
    localFolder = us.mConfig.getLocalPath();
    state = BackupInfoSync::getSyncState(us, pauseDown, pauseUp);
    subState = us.mConfig.mError;
    deviceId = us.syncs.mClient.getDeviceidHash();
    driveId = BackupInfoSync::getDriveId(us);
    assert(!(us.mConfig.isBackup() && us.mConfig.isExternal())  // not an external backup...
           || !ISUNDEF(driveId));  // ... or it must have a valid drive-id
}

CommandBackupPut::SPState BackupInfoSync::calculatePauseActiveState(bool pauseDown, bool pauseUp)
{
    if (pauseDown && pauseUp)
    {
        return CommandBackupPut::PAUSE_FULL;
    }
    else if (pauseDown)
    {
        return CommandBackupPut::PAUSE_DOWN;
    }
    else if (pauseUp)
    {
        return CommandBackupPut::PAUSE_UP;
    }

    return CommandBackupPut::ACTIVE;
}

CommandBackupPut::SPState BackupInfoSync::getSyncState(const UnifiedSync& us, bool pauseDown, bool pauseUp)
{
    SyncError error = us.mConfig.mError;
    syncstate_t state = us.mSync ? us.mSync->state() : SYNC_FAILED;

    return getSyncState(error, state, pauseDown, pauseUp);
}

CommandBackupPut::SPState BackupInfoSync::getSyncState(SyncError error, syncstate_t state, bool pauseDown, bool pauseUp)
{
    if (state == SYNC_DISABLED && error != NO_SYNC_ERROR)
    {
        return CommandBackupPut::TEMPORARY_DISABLED;
    }
    else if (state != SYNC_FAILED && state != SYNC_CANCELED && state != SYNC_DISABLED)
    {
        return calculatePauseActiveState(pauseDown, pauseUp);
    }
    else if (!(state != SYNC_CANCELED && (state != SYNC_DISABLED || error != NO_SYNC_ERROR)))
    {
        return CommandBackupPut::DISABLED;
    }
    else
    {
        return CommandBackupPut::FAILED;
    }
}

CommandBackupPut::SPState BackupInfoSync::getSyncState(const SyncConfig& config, bool pauseDown, bool pauseUp)
{
    auto error = config.mError;
    if (!error)
    {
        if (config.getEnabled())
        {
            return calculatePauseActiveState(pauseDown, pauseUp);
        }
        else
        {
            return CommandBackupPut::DISABLED;
        }
    }
    else //error
    {
        if (config.getEnabled())
        {
            return CommandBackupPut::TEMPORARY_DISABLED;
        }
        else
        {
            return CommandBackupPut::DISABLED;
        }
    }
}

handle BackupInfoSync::getDriveId(const UnifiedSync &us)
{
    const auto& drivePath = us.mConfig.mExternalDrivePath;

    // Only external drives have drive IDs.
    if (drivePath.empty())
        return UNDEF;

    // Get our hands on the config store.
    const auto store = us.syncs.syncConfigStore();

    // It should always be available.
    assert(store);

    // Drive should be known.
    assert(store->driveKnown(drivePath));

    // Ask the store for the drive's backup ID.
    auto id = store->driveID(drivePath);

    // It should never be undefined.
    assert(id != UNDEF);

    return id;
}

BackupType BackupInfoSync::getSyncType(const SyncConfig& config)
{
    switch (config.getType())
    {
    case SyncConfig::TYPE_UP:
            return BackupType::UP_SYNC;
    case SyncConfig::TYPE_DOWN:
            return BackupType::DOWN_SYNC;
    case SyncConfig::TYPE_TWOWAY:
            return BackupType::TWO_WAY;
    case SyncConfig::TYPE_BACKUP:
            return BackupType::BACKUP_UPLOAD;
    default:
            return BackupType::INVALID;
    }
}

BackupMonitor::BackupMonitor(Syncs& s)
    : syncs(s)
{
}

void BackupMonitor::updateOrRegisterSync(UnifiedSync& us)
{
    assert(syncs.onSyncThread());

    if (us.mConfig.mSyncDeregisterSent) return;

#ifndef NDEBUG
    handle backupId = us.mConfig.mBackupId;
    assert(!ISUNDEF(backupId)); // syncs are registered before adding them
#endif

    auto currentInfo = BackupInfoSync(us, syncs.mDownloadsPaused, syncs.mUploadsPaused);
    if (!us.mBackupInfo || currentInfo != *us.mBackupInfo)
    {
        syncs.queueClient([currentInfo](MegaClient& mc, DBTableTransactionCommitter& committer)
            {
                mc.reqs.add(new CommandBackupPut(&mc, currentInfo, nullptr));
            });
    }
    us.mBackupInfo = ::mega::make_unique<BackupInfoSync>(currentInfo);
}

bool BackupInfoSync::operator==(const BackupInfoSync& o) const
{
    return  backupId == o.backupId &&
            driveId == o.driveId &&
            type == o.type &&
            backupName == o.backupName &&
            nodeHandle == o.nodeHandle &&
            localFolder == o.localFolder &&
            deviceId == o.deviceId &&
            state == o.state &&
            subState == o.subState;
}

bool BackupInfoSync::operator!=(const BackupInfoSync &o) const
{
    return !(*this == o);
}

void BackupMonitor::beatBackupInfo(UnifiedSync& us)
{
    assert(syncs.onSyncThread());

    if (us.mConfig.mSyncDeregisterSent) return;

    // send registration or update in case we missed it
    updateOrRegisterSync(us);

    if (ISUNDEF(us.mConfig.mBackupId))
    {
        LOG_warn << "Backup not registered yet. Skipping heartbeat...";
        return;
    }

    std::shared_ptr<HeartBeatSyncInfo> hbs = us.mNextHeartbeat;

    if (us.mSync)
    {
        auto counts = us.mSync->threadSafeState->transferCounts();

        if (hbs->mSnapshotTransferCounts != counts)
        {
            hbs->mSnapshotTransferCounts = counts;
            hbs->updateLastActionTime();
        }
    }

    hbs->updateSPHBStatus(us);

    auto elapsedSec = m_time(nullptr) - hbs->lastBeat();

    if ( !hbs->mSending &&
         (elapsedSec >= MAX_HEARBEAT_SECS_DELAY ||
         (elapsedSec*10 >= FREQUENCY_HEARTBEAT_DS && hbs->mModified)))
    {

        hbs->setLastBeat(m_time(nullptr));

        m_off_t inflightProgress = 0;
        if (us.mSync)
        {
            inflightProgress = us.mSync->getInflightProgress();
        }

        auto reportCounts = hbs->mSnapshotTransferCounts;
        reportCounts -= hbs->mResolvedTransferCounts;

        auto progress = uint8_t(100.0 * reportCounts.progress(inflightProgress));

        hbs->mSending = true;

        auto backupId = us.mConfig.mBackupId;
        auto status = hbs->sphbStatus();
        auto pendingUps = static_cast<uint32_t>(reportCounts.mUploads.mPending);
        auto pendingDowns = static_cast<uint32_t>(reportCounts.mUploads.mPending);
        auto lastAction = hbs->lastAction();
        auto lastItemUpdated = hbs->lastItemUpdated();

        syncs.queueClient([=](MegaClient& mc, DBTableTransactionCommitter& committer)
            {
                mc.reqs.add(
                    new CommandBackupPutHeartBeat(&mc, backupId, status,
                        progress, pendingUps, pendingDowns,
                        lastAction, lastItemUpdated,
                        [hbs](Error){
                            hbs->mSending = false;
                        }));
            });

        if (progress >= 100)
        {
            // once we reach 100%, start counting again from 0 for any later sync activity.
            hbs->mResolvedTransferCounts = hbs->mSnapshotTransferCounts;
        }
    }
}

void BackupMonitor::beat()
{
    assert(syncs.onSyncThread());

    // Only send heartbeats for enabled active syncs.
    for (auto& us : syncs.mSyncVec)
    {
        if (us->mSync && us->mConfig.getEnabled())
        {
            beatBackupInfo(*us);
        }
    };
}

#endif

}
