/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsAutoSyncManager.h"
#include "nsAutoSyncState.h"
#include "nsIMsgImapMailFolder.h"
#include "nsIMsgHdr.h"
#include "nsIObserverService.h"
#include "nsIMsgAccountManager.h"
#include "nsIMsgIncomingServer.h"
#include "nsIImapIncomingServer.h"
#include "nsIMsgMailSession.h"
#include "nsMsgFolderFlags.h"
#include "nsMsgMessageFlags.h"
#include "nsMsgUtils.h"
#include "nsIIOService.h"
#include "nsITimer.h"
#include "nsComponentManagerUtils.h"
#include "nsServiceManagerUtils.h"
#include "mozilla/Services.h"
#include "mozilla/Logging.h"

using namespace mozilla;

NS_IMPL_ISUPPORTS(nsDefaultAutoSyncMsgStrategy, nsIAutoSyncMsgStrategy)

const char* kAppIdleNotification = "mail:appIdle";
const char* kStartupDoneNotification = "mail-startup-done";
LazyLogModule gAutoSyncLog("IMAPAutoSync");

// recommended size of each group of messages per download
static const uint32_t kDefaultGroupSize = 50U * 1024U /* 50K */;

nsDefaultAutoSyncMsgStrategy::nsDefaultAutoSyncMsgStrategy() {}

nsDefaultAutoSyncMsgStrategy::~nsDefaultAutoSyncMsgStrategy() {}

NS_IMETHODIMP nsDefaultAutoSyncMsgStrategy::Sort(
    nsIMsgFolder* aFolder, nsIMsgDBHdr* aMsgHdr1, nsIMsgDBHdr* aMsgHdr2,
    nsAutoSyncStrategyDecisionType* aDecision) {
  NS_ENSURE_ARG_POINTER(aDecision);

  uint32_t msgSize1 = 0, msgSize2 = 0;
  PRTime msgDate1 = 0, msgDate2 = 0;

  if (!aMsgHdr1 || !aMsgHdr2) {
    *aDecision = nsAutoSyncStrategyDecisions::Same;
    return NS_OK;
  }

  aMsgHdr1->GetMessageSize(&msgSize1);
  aMsgHdr1->GetDate(&msgDate1);

  aMsgHdr2->GetMessageSize(&msgSize2);
  aMsgHdr2->GetDate(&msgDate2);

  // Special case: if message size is larger than a
  // certain size, then place it to the bottom of the q
  if (msgSize2 > kFirstPassMessageSize && msgSize1 > kFirstPassMessageSize)
    *aDecision = msgSize2 > msgSize1 ? nsAutoSyncStrategyDecisions::Lower
                                     : nsAutoSyncStrategyDecisions::Higher;
  else if (msgSize2 > kFirstPassMessageSize)
    *aDecision = nsAutoSyncStrategyDecisions::Lower;
  else if (msgSize1 > kFirstPassMessageSize)
    *aDecision = nsAutoSyncStrategyDecisions::Higher;
  else {
    // Most recent and smallest first
    if (msgDate1 < msgDate2)
      *aDecision = nsAutoSyncStrategyDecisions::Higher;
    else if (msgDate1 > msgDate2)
      *aDecision = nsAutoSyncStrategyDecisions::Lower;
    else {
      if (msgSize1 > msgSize2)
        *aDecision = nsAutoSyncStrategyDecisions::Higher;
      else if (msgSize1 < msgSize2)
        *aDecision = nsAutoSyncStrategyDecisions::Lower;
      else
        *aDecision = nsAutoSyncStrategyDecisions::Same;
    }
  }
  return NS_OK;
}

NS_IMETHODIMP nsDefaultAutoSyncMsgStrategy::IsExcluded(nsIMsgFolder* aFolder,
                                                       nsIMsgDBHdr* aMsgHdr,
                                                       bool* aDecision) {
  NS_ENSURE_ARG_POINTER(aDecision);
  NS_ENSURE_ARG_POINTER(aMsgHdr);
  NS_ENSURE_ARG_POINTER(aFolder);
  nsCOMPtr<nsIMsgIncomingServer> server;

  nsresult rv = aFolder->GetServer(getter_AddRefs(server));
  NS_ENSURE_SUCCESS(rv, rv);
  nsCOMPtr<nsIImapIncomingServer> imapServer(do_QueryInterface(server, &rv));
  int32_t offlineMsgAgeLimit = -1;
  imapServer->GetAutoSyncMaxAgeDays(&offlineMsgAgeLimit);
  NS_ENSURE_SUCCESS(rv, rv);
  PRTime msgDate;
  aMsgHdr->GetDate(&msgDate);
  *aDecision = offlineMsgAgeLimit > 0 &&
               msgDate < MsgConvertAgeInDaysToCutoffDate(offlineMsgAgeLimit);
  return NS_OK;
}

NS_IMPL_ISUPPORTS(nsDefaultAutoSyncFolderStrategy, nsIAutoSyncFolderStrategy)

nsDefaultAutoSyncFolderStrategy::nsDefaultAutoSyncFolderStrategy() {}

nsDefaultAutoSyncFolderStrategy::~nsDefaultAutoSyncFolderStrategy() {}

NS_IMETHODIMP nsDefaultAutoSyncFolderStrategy::Sort(
    nsIMsgFolder* aFolderA, nsIMsgFolder* aFolderB,
    nsAutoSyncStrategyDecisionType* aDecision) {
  NS_ENSURE_ARG_POINTER(aDecision);

  if (!aFolderA || !aFolderB) {
    *aDecision = nsAutoSyncStrategyDecisions::Same;
    return NS_OK;
  }

  bool isInbox1, isInbox2, isDrafts1, isDrafts2, isTrash1, isTrash2;
  aFolderA->GetFlag(nsMsgFolderFlags::Inbox, &isInbox1);
  aFolderB->GetFlag(nsMsgFolderFlags::Inbox, &isInbox2);
  //
  aFolderA->GetFlag(nsMsgFolderFlags::Drafts, &isDrafts1);
  aFolderB->GetFlag(nsMsgFolderFlags::Drafts, &isDrafts2);
  //
  aFolderA->GetFlag(nsMsgFolderFlags::Trash, &isTrash1);
  aFolderB->GetFlag(nsMsgFolderFlags::Trash, &isTrash2);

  // Follow this order;
  // INBOX > DRAFTS > SUBFOLDERS > TRASH

  // test whether the folder is opened by the user.
  // we give high priority to the folders explicitly opened by
  // the user.
  nsresult rv;
  bool folderAOpen = false;
  bool folderBOpen = false;
  nsCOMPtr<nsIMsgMailSession> session =
      do_GetService("@mozilla.org/messenger/services/session;1", &rv);
  if (NS_SUCCEEDED(rv) && session) {
    session->IsFolderOpenInWindow(aFolderA, &folderAOpen);
    session->IsFolderOpenInWindow(aFolderB, &folderBOpen);
  }

  if (folderAOpen == folderBOpen) {
    // if both of them or none of them are opened by the user
    // make your decision based on the folder type
    if (isInbox2 || (isDrafts2 && !isInbox1) || isTrash1)
      *aDecision = nsAutoSyncStrategyDecisions::Higher;
    else if (isInbox1 || (isDrafts1 && !isDrafts2) || isTrash2)
      *aDecision = nsAutoSyncStrategyDecisions::Lower;
    else
      *aDecision = nsAutoSyncStrategyDecisions::Same;
  } else {
    // otherwise give higher priority to opened one
    *aDecision = folderBOpen ? nsAutoSyncStrategyDecisions::Higher
                             : nsAutoSyncStrategyDecisions::Lower;
  }

  return NS_OK;
}

NS_IMETHODIMP
nsDefaultAutoSyncFolderStrategy::IsExcluded(nsIMsgFolder* aFolder,
                                            bool* aDecision) {
  NS_ENSURE_ARG_POINTER(aDecision);
  NS_ENSURE_ARG_POINTER(aFolder);
  uint32_t folderFlags;
  aFolder->GetFlags(&folderFlags);
  // exclude saved search
  *aDecision = (folderFlags & nsMsgFolderFlags::Virtual);
  if (!*aDecision) {
    // Exclude orphans
    nsCOMPtr<nsIMsgFolder> parent;
    aFolder->GetParent(getter_AddRefs(parent));
    if (!parent) *aDecision = true;
  }
  return NS_OK;
}

#define NOTIFY_LISTENERS_STATIC(obj_, propertyfunc_, params_)               \
  PR_BEGIN_MACRO                                                            \
  nsTObserverArray<nsCOMPtr<nsIAutoSyncMgrListener>>::ForwardIterator iter( \
      obj_->mListeners);                                                    \
  nsCOMPtr<nsIAutoSyncMgrListener> listener;                                \
  while (iter.HasMore()) {                                                  \
    listener = iter.GetNext();                                              \
    listener->propertyfunc_ params_;                                        \
  }                                                                         \
  PR_END_MACRO

#define NOTIFY_LISTENERS(propertyfunc_, params_) \
  NOTIFY_LISTENERS_STATIC(this, propertyfunc_, params_)

nsAutoSyncManager::nsAutoSyncManager() {
  mGroupSize = kDefaultGroupSize;

  mIdleState = notIdle;
  mStartupDone = false;
  mDownloadModel = dmChained;
  mUpdateInProgress = false;
  mPaused = false;

  nsresult rv;
  mIdleService = do_GetService("@mozilla.org/widget/useridleservice;1", &rv);
  if (mIdleService) mIdleService->AddIdleObserver(this, kIdleTimeInSec);

  // Observe xpcom-shutdown event and app-idle changes
  nsCOMPtr<nsIObserverService> observerService =
      mozilla::services::GetObserverService();

  rv = observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
  observerService->AddObserver(this, kAppIdleNotification, false);
  observerService->AddObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false);
  observerService->AddObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC, false);
  observerService->AddObserver(this, kStartupDoneNotification, false);
}

nsAutoSyncManager::~nsAutoSyncManager() {}

void nsAutoSyncManager::InitTimer() {
  if (!mTimer) {
    nsresult rv = NS_NewTimerWithFuncCallback(
        getter_AddRefs(mTimer), TimerCallback, (void*)this, kTimerIntervalInMs,
        nsITimer::TYPE_REPEATING_SLACK, "nsAutoSyncManager::TimerCallback",
        nullptr);
    if (NS_FAILED(rv)) {
      NS_WARNING("Could not start nsAutoSyncManager timer");
    }
  }
}

void nsAutoSyncManager::StopTimer() {
  if (mTimer) {
    mTimer->Cancel();
    mTimer = nullptr;
  }
}

void nsAutoSyncManager::StartTimerIfNeeded() {
  if ((mUpdateQ.Count() > 0 || mDiscoveryQ.Count() > 0) && !mTimer) InitTimer();
}

void nsAutoSyncManager::TimerCallback(nsITimer* aTimer, void* aClosure) {
  if (!aClosure) return;

  nsAutoSyncManager* autoSyncMgr = static_cast<nsAutoSyncManager*>(aClosure);
  if (autoSyncMgr->GetIdleState() == notIdle ||
      (autoSyncMgr->mDiscoveryQ.Count() <= 0 &&
       autoSyncMgr->mUpdateQ.Count() <= 0)) {
    // Idle will create a new timer automatically if discovery Q or update Q is
    // not empty
    autoSyncMgr->StopTimer();
  }

  // process a folder in the discovery queue
  if (autoSyncMgr->mDiscoveryQ.Count() > 0) {
    nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(autoSyncMgr->mDiscoveryQ[0]);
    if (autoSyncStateObj) {
      uint32_t leftToProcess;
      nsresult rv = autoSyncStateObj->ProcessExistingHeaders(
          kNumberOfHeadersToProcess, &leftToProcess);

      nsCOMPtr<nsIMsgFolder> folder;
      autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
      if (folder)
        NOTIFY_LISTENERS_STATIC(
            autoSyncMgr, OnDiscoveryQProcessed,
            (folder, kNumberOfHeadersToProcess, leftToProcess));

      if (NS_SUCCEEDED(rv) && 0 == leftToProcess) {
        autoSyncMgr->mDiscoveryQ.RemoveObjectAt(0);
        if (folder)
          NOTIFY_LISTENERS_STATIC(
              autoSyncMgr, OnFolderRemovedFromQ,
              (nsIAutoSyncMgrListener::DiscoveryQueue, folder));
      }
      if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
        nsCString folderName;
        folder->GetURI(folderName);
        MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
                ("%s: processed discovery q for folder=%s, "
                 "msgs left to process in folder=%d",
                 __func__, folderName.get(), leftToProcess));
      }
    }
  }

  if (autoSyncMgr->mUpdateQ.Count() > 0) {
    if (!autoSyncMgr->mUpdateInProgress)  // Avoids possible overlap of updates
    {
      nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(autoSyncMgr->mUpdateQ[0]);
      if (autoSyncStateObj) {
        int32_t state;
        nsresult rv = autoSyncStateObj->GetState(&state);
        if (NS_SUCCEEDED(rv) && (state == nsAutoSyncState::stCompletedIdle ||
                                 state == nsAutoSyncState::stUpdateNeeded)) {
          nsCOMPtr<nsIMsgFolder> folder;
          autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
          if (folder) {
            nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
                do_QueryInterface(folder, &rv);
            NS_ENSURE_SUCCESS_VOID(rv);
            rv = imapFolder->InitiateAutoSync(autoSyncMgr);
            if (NS_SUCCEEDED(rv)) {
              autoSyncMgr->mUpdateInProgress = true;
              NOTIFY_LISTENERS_STATIC(autoSyncMgr, OnAutoSyncInitiated,
                                      (folder));
            }
          }
          if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
            nsCString folderName;
            folder->GetURI(folderName);
            MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
                    ("%s: process update q for folder=%s", __func__,
                     folderName.get()));
          }
        }
      }
    }
    // if initiation is not successful for some reason, or
    // if there is an on going download for this folder,
    // remove it from q and continue with the next one
    if (!autoSyncMgr->mUpdateInProgress) {
      nsCOMPtr<nsIMsgFolder> folder;
      autoSyncMgr->mUpdateQ[0]->GetOwnerFolder(getter_AddRefs(folder));

      autoSyncMgr->mUpdateQ.RemoveObjectAt(0);

      if (folder)
        NOTIFY_LISTENERS_STATIC(autoSyncMgr, OnFolderRemovedFromQ,
                                (nsIAutoSyncMgrListener::UpdateQueue, folder));
      if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Error)) {
        nsCString folderName;
        folder->GetURI(folderName);
        MOZ_LOG(gAutoSyncLog, LogLevel::Error,
                ("%s: update q init failed for folder=%s", __func__,
                 folderName.get()));
      }
    }

  }  // endif
}

/**
 * Populates aChainedQ with the auto-sync state objects that are not owned by
 * the same imap server.
 * Assumes that aChainedQ initially empty.
 */
void nsAutoSyncManager::ChainFoldersInQ(
    const nsCOMArray<nsIAutoSyncState>& aQueue,
    nsCOMArray<nsIAutoSyncState>& aChainedQ) {
  if (aQueue.Count() > 0) aChainedQ.AppendObject(aQueue[0]);

  int32_t pqElemCount = aQueue.Count();
  for (int32_t pqidx = 1; pqidx < pqElemCount; pqidx++) {
    bool chained = false;
    int32_t needToBeReplacedWith = -1;
    int32_t elemCount = aChainedQ.Count();
    for (int32_t idx = 0; idx < elemCount; idx++) {
      bool isSibling;
      nsresult rv = aChainedQ[idx]->IsSibling(aQueue[pqidx], &isSibling);

      if (NS_SUCCEEDED(rv) && isSibling) {
        // this prevent us to overwrite a lower priority sibling in
        // download-in-progress state with a higher priority one.
        // we have to wait until its download is completed before
        // switching to new one.
        int32_t state;
        aQueue[pqidx]->GetState(&state);
        if (aQueue[pqidx] != aChainedQ[idx] &&
            state == nsAutoSyncState::stDownloadInProgress)
          needToBeReplacedWith = idx;
        else
          chained = true;

        break;
      }
    }  // endfor

    if (needToBeReplacedWith > -1)
      aChainedQ.ReplaceObjectAt(aQueue[pqidx], needToBeReplacedWith);
    else if (!chained)
      aChainedQ.AppendObject(aQueue[pqidx]);

  }  // endfor
}

/**
 * Searches the given queue for another folder owned by the same imap server.
 */
nsIAutoSyncState* nsAutoSyncManager::SearchQForSibling(
    const nsCOMArray<nsIAutoSyncState>& aQueue,
    nsIAutoSyncState* aAutoSyncStateObj, int32_t aStartIdx, int32_t* aIndex) {
  if (aIndex) *aIndex = -1;

  if (aAutoSyncStateObj) {
    bool isSibling;
    int32_t elemCount = aQueue.Count();
    for (int32_t idx = aStartIdx; idx < elemCount; idx++) {
      nsresult rv = aAutoSyncStateObj->IsSibling(aQueue[idx], &isSibling);

      if (NS_SUCCEEDED(rv) && isSibling && aAutoSyncStateObj != aQueue[idx]) {
        if (aIndex) *aIndex = idx;

        return aQueue[idx];
      }
    }
  }
  return nullptr;
}

/**
 * Searches for the next folder owned by the same imap server in the given
 * queue, starting from the index of the given folder.
 */
nsIAutoSyncState* nsAutoSyncManager::GetNextSibling(
    const nsCOMArray<nsIAutoSyncState>& aQueue,
    nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex) {
  if (aIndex) *aIndex = -1;

  if (aAutoSyncStateObj) {
    bool located = false;
    bool isSibling;
    int32_t elemCount = aQueue.Count();
    for (int32_t idx = 0; idx < elemCount; idx++) {
      if (!located) {
        located = (aAutoSyncStateObj == aQueue[idx]);
        continue;
      }

      nsresult rv = aAutoSyncStateObj->IsSibling(aQueue[idx], &isSibling);
      if (NS_SUCCEEDED(rv) && isSibling) {
        if (aIndex) *aIndex = idx;

        return aQueue[idx];
      }
    }
  }
  return nullptr;
}

/**
 * Checks whether there is another folder in the given q that is owned
 * by the same imap server or not.
 *
 * @param aQueue the queue that will be searched for a sibling
 * @param aAutoSyncStateObj the auto-sync state object that we are looking
 *                          a sibling for
 * @param aState the state of the sibling. -1 means "any state"
 * @param aIndex [out] the index of the found sibling, if it is provided by the
 *               caller (not null)
 * @return true if found, false otherwise
 */
bool nsAutoSyncManager::DoesQContainAnySiblingOf(
    const nsCOMArray<nsIAutoSyncState>& aQueue,
    nsIAutoSyncState* aAutoSyncStateObj, const int32_t aState,
    int32_t* aIndex) {
  if (aState == -1)
    return (nullptr != SearchQForSibling(aQueue, aAutoSyncStateObj, 0, aIndex));

  int32_t offset = 0;
  nsIAutoSyncState* autoSyncState;
  while ((autoSyncState =
              SearchQForSibling(aQueue, aAutoSyncStateObj, offset, &offset))) {
    int32_t state;
    nsresult rv = autoSyncState->GetState(&state);
    if (NS_SUCCEEDED(rv) && aState == state) break;

    offset++;
  }
  if (aIndex) *aIndex = offset;

  return (nullptr != autoSyncState);
}

/**
 * Searches the given queue for the highest priority folder owned by the
 * same imap server.
 */
nsIAutoSyncState* nsAutoSyncManager::GetHighestPrioSibling(
    const nsCOMArray<nsIAutoSyncState>& aQueue,
    nsIAutoSyncState* aAutoSyncStateObj, int32_t* aIndex) {
  return SearchQForSibling(aQueue, aAutoSyncStateObj, 0, aIndex);
}

// to chain update folder actions
NS_IMETHODIMP nsAutoSyncManager::OnStartRunningUrl(nsIURI* aUrl) {
  return NS_OK;
}

/**
 * This is called when an update folder URL finishes. It is also called by
 * nsAutoSyncState::OnStopRunningUrl when a folder status URL finishes.
 */
NS_IMETHODIMP nsAutoSyncManager::OnStopRunningUrl(nsIURI* aUrl,
                                                  nsresult aExitCode) {
  if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
    nsCString uri;
    if (aUrl) uri = aUrl->GetSpecOrDefault();
    MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
            ("nsAutoSyncManager::%s, count=%d, url=%s", __func__,
             mUpdateQ.Count(), uri.get()));
  }
  mUpdateInProgress = false;  // Set false to allow next folder to update
  if (mUpdateQ.Count() > 0) mUpdateQ.RemoveObjectAt(0);

  return aExitCode;
}

/**
 * This occurs on system sleep, hibernate or when TB is set offline or shutdown.
 */
NS_IMETHODIMP nsAutoSyncManager::Pause() {
  StopTimer();
  mPaused = true;
  MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("autosync paused"));
  return NS_OK;
}

/**
 * This occurs on wakeup from sleep or hibernate and when TB is returned online.
 */
NS_IMETHODIMP nsAutoSyncManager::Resume() {
  mPaused = false;
  StartTimerIfNeeded();
  // If mUpdateInProgress was true on resume it needs to be reset back to false
  // to avoid inhibiting autosync until a restart. OnStopRunningUrl(), where it
  // is normally reset, may not occur depending on timing and autosync will
  // never be initiated in TimerCallback() for any folder.
  MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
          ("autosync resumed, mUpdateInProgress=%d(bool)", mUpdateInProgress));
  mUpdateInProgress = false;  // May already be false, that's OK
  return NS_OK;
}

NS_IMETHODIMP nsAutoSyncManager::Observe(nsISupports*, const char* aTopic,
                                         const char16_t* aSomeData) {
  if (!PL_strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
    nsCOMPtr<nsIObserverService> observerService =
        mozilla::services::GetObserverService();
    if (observerService) {
      observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
      observerService->RemoveObserver(this, kAppIdleNotification);
      observerService->RemoveObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
      observerService->RemoveObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC);
      observerService->RemoveObserver(this, kStartupDoneNotification);
    }

    // cancel and release the timer
    if (mTimer) {
      mTimer->Cancel();
      mTimer = nullptr;
    }
    // unsubscribe from idle service
    if (mIdleService) mIdleService->RemoveIdleObserver(this, kIdleTimeInSec);

    return NS_OK;
  }

  if (!PL_strcmp(aTopic, kStartupDoneNotification)) {
    mStartupDone = true;
  } else if (!PL_strcmp(aTopic, kAppIdleNotification)) {
    if (nsDependentString(aSomeData).EqualsLiteral("idle")) {
      IdleState prevIdleState = GetIdleState();

      // we were already idle (either system or app), so
      // just remember that we're app idle and return.
      SetIdleState(appIdle);
      if (prevIdleState != notIdle) return NS_OK;

      MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: in app idle", __func__));
      return StartIdleProcessing();
    }

    // we're back from appIdle - if already notIdle, just return;
    if (GetIdleState() == notIdle) return NS_OK;
    MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: out of app idle", __func__));

    SetIdleState(notIdle);
    NOTIFY_LISTENERS(OnStateChanged, (false));
    return NS_OK;
  } else if (!PL_strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC)) {
    if (nsDependentString(aSomeData).EqualsLiteral(NS_IOSERVICE_ONLINE))
      Resume();
  } else if (!PL_strcmp(aTopic, NS_IOSERVICE_GOING_OFFLINE_TOPIC)) {
    Pause();
  }
  // we're back from system idle
  else if (!PL_strcmp(aTopic, "back")) {
    // if we're app idle when we get back from system idle, we ignore
    // it, since we'll keep doing our idle stuff.
    if (GetIdleState() != appIdle) {
      SetIdleState(notIdle);
      NOTIFY_LISTENERS(OnStateChanged, (false));
      MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: out of idle", __func__));
    }
    return NS_OK;
  } else  // we've gone system idle
  {
    // Check if we were already idle. We may have gotten
    // multiple system idle notificatons. In that case,
    // just remember that we're systemIdle and return;
    if (GetIdleState() != notIdle) return NS_OK;

    // we might want to remember if we were app idle, because
    // coming back from system idle while app idle shouldn't stop
    // app indexing. But I think it's OK for now just leave ourselves
    // in appIdle state.
    if (GetIdleState() != appIdle) SetIdleState(systemIdle);
    if (WeAreOffline()) return NS_OK;
    MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("%s: in sys idle", __func__));
    return StartIdleProcessing();
  }
  return NS_OK;
}

nsresult nsAutoSyncManager::StartIdleProcessing() {
  if (mPaused) return NS_OK;

  MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("enter %s", __func__));
  StartTimerIfNeeded();

  // Ignore idle events sent during the startup
  if (!mStartupDone) return NS_OK;

  // notify listeners that auto-sync is running
  NOTIFY_LISTENERS(OnStateChanged, (true));

  nsCOMArray<nsIAutoSyncState> chainedQ;
  nsCOMArray<nsIAutoSyncState>* queue = &mPriorityQ;
  if (mDownloadModel == dmChained) {
    ChainFoldersInQ(mPriorityQ, chainedQ);
    queue = &chainedQ;
  }

  // to store the folders that should be removed from the priority
  // queue at the end of the iteration.
  nsCOMArray<nsIAutoSyncState> foldersToBeRemoved;

  // process folders in the priority queue
  int32_t elemCount = queue->Count();
  for (int32_t idx = 0; idx < elemCount; idx++) {
    nsCOMPtr<nsIAutoSyncState> autoSyncStateObj((*queue)[idx]);
    if (!autoSyncStateObj) continue;

    int32_t state;
    autoSyncStateObj->GetState(&state);

    // TODO: Test cached-connection availability in parallel mode
    // and do not exceed (cached-connection count - 1)

    if (state != nsAutoSyncState::stReadyToDownload) continue;

    nsresult rv = DownloadMessagesForOffline(autoSyncStateObj);
    if (NS_FAILED(rv)) {
      // special case: this folder does not have any message to download
      // (see bug 457342), remove it explicitly from the queue when iteration
      // is over.
      // Note that in normal execution flow, folders are removed from priority
      // queue only in OnDownloadCompleted when all messages are downloaded
      // successfully. This is the only place we change this flow.
      if (NS_ERROR_NOT_AVAILABLE == rv)
        foldersToBeRemoved.AppendObject(autoSyncStateObj);

      HandleDownloadErrorFor(autoSyncStateObj, rv);
    }  // endif
  }    // endfor

  // remove folders with no pending messages from the priority queue
  elemCount = foldersToBeRemoved.Count();
  for (int32_t idx = 0; idx < elemCount; idx++) {
    nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(foldersToBeRemoved[idx]);
    if (!autoSyncStateObj) continue;

    nsCOMPtr<nsIMsgFolder> folder;
    autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
    if (folder) NOTIFY_LISTENERS(OnDownloadCompleted, (folder));

    if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
      nsCString folderName;
      folder->GetURI(folderName);
      MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
              ("%s: folder=%s has no pending msgs, "
               "remove from priority q",
               __func__, folderName.get()));
    }
    autoSyncStateObj->SetState(nsAutoSyncState::stCompletedIdle);

    if (mPriorityQ.RemoveObject(autoSyncStateObj))
      NOTIFY_LISTENERS(OnFolderRemovedFromQ,
                       (nsIAutoSyncMgrListener::PriorityQueue, folder));
  }

  return AutoUpdateFolders();
}

/**
 * Updates offline imap folders that are not synchronized recently. This is
 * called whenever we're idle.
 */
nsresult nsAutoSyncManager::AutoUpdateFolders() {
  nsresult rv;

  // iterate through each imap account and update offline folders automatically
  MOZ_LOG(gAutoSyncLog, LogLevel::Debug, ("enter %s", __func__));

  nsCOMPtr<nsIMsgAccountManager> accountManager =
      do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsTArray<RefPtr<nsIMsgAccount>> accounts;
  rv = accountManager->GetAccounts(accounts);
  NS_ENSURE_SUCCESS(rv, rv);

  for (auto account : accounts) {
    if (!account) continue;

    nsCOMPtr<nsIMsgIncomingServer> incomingServer;
    rv = account->GetIncomingServer(getter_AddRefs(incomingServer));
    if (!incomingServer) continue;

    nsCString type;
    rv = incomingServer->GetType(type);

    if (!type.EqualsLiteral("imap")) continue;

    // If we haven't logged onto this server yet during this session or if the
    // password has been removed from cache (see
    // nsImapIncomingServer::ForgetSessionPassword) then skip autosync for
    // this account.
    bool notLoggedIn;
    incomingServer->GetServerRequiresPasswordForBiff(&notLoggedIn);
    if (notLoggedIn) {
      if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
        nsCString serverName;
        incomingServer->GetHostName(serverName);
        MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
                ("%s: server |%s| don't autosync; not yet logged in", __func__,
                 serverName.get()));
      }
      continue;
    }

    nsCOMPtr<nsIMsgFolder> rootFolder;

    rv = incomingServer->GetRootFolder(getter_AddRefs(rootFolder));
    if (rootFolder) {
      if (NS_FAILED(rv)) continue;

      nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
      rv = rootFolder->GetDescendants(allDescendants);

      // Get the update time in minutes for each folder of this account/server.
      // It will be the user configured biff time for server even if user has
      // disabled "Check for new messages every X minutes" for the account.
      // Update time will default to 10 minutes if an invalid value is set or
      // if there are errors obtaining it.
      // Specifically, the value used here is mail.server.serverX.check_time
      // or the default mail.server.default.check_time.
      int32_t updateMinutes = -1;
      rv = incomingServer->GetBiffMinutes(&updateMinutes);
      if (NS_FAILED(rv) || updateMinutes < 1)
        updateMinutes = kDefaultUpdateInterval;
      PRTime span = updateMinutes * (PR_USEC_PER_SEC * 60UL);
      if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
        nsCString serverName;
        incomingServer->GetHostName(serverName);
        MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
                ("%s: Update time set to |%d| minutes for "
                 "folders in account |%s|",
                 __func__, updateMinutes, serverName.get()));
      }

      for (auto folder : allDescendants) {
        uint32_t folderFlags;
        rv = folder->GetFlags(&folderFlags);
        // Skip this folder if not offline or is a saved search or is no select.
        if (NS_FAILED(rv) || !(folderFlags & nsMsgFolderFlags::Offline) ||
            folderFlags &
                (nsMsgFolderFlags::Virtual | nsMsgFolderFlags::ImapNoselect))
          continue;

        nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
            do_QueryInterface(folder, &rv);
        if (NS_FAILED(rv)) continue;

        nsCOMPtr<nsIImapIncomingServer> imapServer;
        rv = imapFolder->GetImapIncomingServer(getter_AddRefs(imapServer));
        if (imapServer) {
          bool autoSyncOfflineStores = false;
          rv = imapServer->GetAutoSyncOfflineStores(&autoSyncOfflineStores);

          // skip if AutoSyncOfflineStores pref is not set for this folder
          if (NS_FAILED(rv) || !autoSyncOfflineStores) continue;
        }

        nsCOMPtr<nsIAutoSyncState> autoSyncState;
        rv = imapFolder->GetAutoSyncStateObj(getter_AddRefs(autoSyncState));
        NS_ASSERTION(
            autoSyncState,
            "*** nsAutoSyncState shouldn't be NULL, check owner folder");

        // shouldn't happen but let's be defensive here
        if (!autoSyncState) continue;

        int32_t state;
        rv = autoSyncState->GetState(&state);
        nsCString folderName;
        if (MOZ_LOG_TEST(gAutoSyncLog, LogLevel::Debug)) {
          folder->GetURI(folderName);
          MOZ_LOG(
              gAutoSyncLog, LogLevel::Debug,
              ("%s: folder=%s, state=%d", __func__, folderName.get(), state));
        }
        if (state == nsAutoSyncState::stCompletedIdle ||
            state == nsAutoSyncState::stUpdateNeeded ||
            state == nsAutoSyncState::stUpdateIssued) {
          // Ensure that we wait for at least the "span" time set above before
          // queuing an update of the same folder.
          PRTime lastUpdateTime;
          rv = autoSyncState->GetLastUpdateTime(&lastUpdateTime);
          if (NS_SUCCEEDED(rv) && ((lastUpdateTime + span) < PR_Now())) {
            int32_t idx = mUpdateQ.IndexOf(autoSyncState);
            if (state == nsAutoSyncState::stUpdateIssued) {
              // Handle the case where an update is triggered but nothing is
              // found to download. This can happen after messages are copied
              // or moved between offline folders of the same server or if imap
              // "folderstatus" URL triggers an update but no new messages
              // are detected.
              bool downloadQEmpty;
              autoSyncState->IsDownloadQEmpty(&downloadQEmpty);
              if (downloadQEmpty) {
                MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
                        ("%s: nothing to download for folder %s, "
                         "set state to stCompletedIdle, updateQ idx=%d",
                         __func__, folderName.get(), idx));
                autoSyncState->SetState(nsAutoSyncState::stCompletedIdle);

                // This should already be done by
                // nsAutoSyncManager::OnStopRunningUrl() but set update state to
                // completed and remove folder state object from update queue in
                // case OnStopRunningUrl never occurred.
                mUpdateInProgress = false;
                if (idx > -1) {
                  mUpdateQ.RemoveObjectAt(idx);
                  idx = -1;  // re-q below
                }
              } else {
                MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
                        ("%s: downloadQ not empty. Why? updateQ idx=%d",
                         __func__, idx));
                if (idx > -1) {
                  // Download q not empty and folder still on update q, maybe it
                  // just needs more time so leave update q as it is to update
                  // on next "span" interval. (Never seen this happen.)
                  idx = 0;
                }
              }
            }
            // Now q or re-q the update for this folder unless it's still q'd.
            if (idx < 0) {
              mUpdateQ.AppendObject(autoSyncState);
              MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
                      ("%s: folder=%s added to update q", __func__,
                       folderName.get()));
              if (folder)
                NOTIFY_LISTENERS(OnFolderAddedIntoQ,
                                 (nsIAutoSyncMgrListener::UpdateQueue, folder));
            }
          }
        }

        // Check if time to add folder to discovery q on kAutoSyncFreq (1 hour)
        // time base.
        PRTime lastSyncTime;
        rv = autoSyncState->GetLastSyncTime(&lastSyncTime);
        if (NS_SUCCEEDED(rv) && ((lastSyncTime + kAutoSyncFreq) < PR_Now())) {
          // add this folder into discovery queue to process existing headers
          // and discover messages not downloaded yet
          if (mDiscoveryQ.IndexOf(autoSyncState) == -1) {
            mDiscoveryQ.AppendObject(autoSyncState);
            MOZ_LOG(gAutoSyncLog, LogLevel::Debug,
                    ("%s: folder=%s added to discovery q", __func__,
                     folderName.get()));
            if (folder)
              NOTIFY_LISTENERS(
                  OnFolderAddedIntoQ,
                  (nsIAutoSyncMgrListener::DiscoveryQueue, folder));
          }
        }
      }  // endfor
    }    // endif
  }      // endfor

  // lazily create the timer if there is something to process in the queue
  // when timer is done, it will self destruct
  StartTimerIfNeeded();

  return rv;
}

/**
 * Places the given folder into the priority queue based on active
 * strategy function.
 */
void nsAutoSyncManager::ScheduleFolderForOfflineDownload(
    nsIAutoSyncState* aAutoSyncStateObj) {
  if (aAutoSyncStateObj && (mPriorityQ.IndexOf(aAutoSyncStateObj) == -1)) {
    nsCOMPtr<nsIAutoSyncFolderStrategy> folStrategy;
    GetFolderStrategy(getter_AddRefs(folStrategy));

    if (mPriorityQ.Count() <= 0) {
      // make sure that we don't insert a folder excluded by the given strategy
      nsCOMPtr<nsIMsgFolder> folder;
      aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
      if (folder) {
        bool excluded = false;
        if (folStrategy) folStrategy->IsExcluded(folder, &excluded);

        if (!excluded) {
          mPriorityQ.AppendObject(
              aAutoSyncStateObj);  // insert into the first spot
          NOTIFY_LISTENERS(OnFolderAddedIntoQ,
                           (nsIAutoSyncMgrListener::PriorityQueue, folder));
        }
      }
    } else {
      // find the right spot for the given folder
      uint32_t qidx = mPriorityQ.Count();
      while (qidx > 0) {
        --qidx;

        nsCOMPtr<nsIMsgFolder> folderA, folderB;
        mPriorityQ[qidx]->GetOwnerFolder(getter_AddRefs(folderA));
        aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folderB));

        bool excluded = false;
        if (folderB && folStrategy) folStrategy->IsExcluded(folderB, &excluded);

        if (excluded) break;

        nsAutoSyncStrategyDecisionType decision =
            nsAutoSyncStrategyDecisions::Same;
        if (folderA && folderB && folStrategy)
          folStrategy->Sort(folderA, folderB, &decision);

        if (decision == nsAutoSyncStrategyDecisions::Higher && 0 == qidx)
          mPriorityQ.InsertObjectAt(aAutoSyncStateObj, 0);
        else if (decision == nsAutoSyncStrategyDecisions::Higher)
          continue;
        else if (decision == nsAutoSyncStrategyDecisions::Lower)
          mPriorityQ.InsertObjectAt(aAutoSyncStateObj, qidx + 1);
        else  //  decision == nsAutoSyncStrategyDecisions::Same
          mPriorityQ.InsertObjectAt(aAutoSyncStateObj, qidx);

        NOTIFY_LISTENERS(OnFolderAddedIntoQ,
                         (nsIAutoSyncMgrListener::PriorityQueue, folderB));
        break;
      }  // end while
    }
  }  // endif
}

/**
 * Zero aSizeLimit means no limit
 */
nsresult nsAutoSyncManager::DownloadMessagesForOffline(
    nsIAutoSyncState* aAutoSyncStateObj, uint32_t aSizeLimit) {
  if (!aAutoSyncStateObj) return NS_ERROR_INVALID_ARG;

  int32_t count;
  nsresult rv = aAutoSyncStateObj->GetPendingMessageCount(&count);
  NS_ENSURE_SUCCESS(rv, rv);

  // special case: no more message to download for this folder:
  // see HandleDownloadErrorFor for recovery policy
  if (!count) return NS_ERROR_NOT_AVAILABLE;

  nsTArray<RefPtr<nsIMsgDBHdr>> messagesToDownload;
  uint32_t totalSize = 0;
  rv = aAutoSyncStateObj->GetNextGroupOfMessages(mGroupSize, &totalSize,
                                                 messagesToDownload);
  NS_ENSURE_SUCCESS(rv, rv);

  // there are pending messages but the cumulative size is zero:
  // treat as special case.
  // Note that although it shouldn't happen, we know that sometimes
  // imap servers manifest messages as zero length. By returning
  // NS_ERROR_NOT_AVAILABLE we cause this folder to be removed from
  // the priority queue temporarily (until the next idle or next update)
  // in an effort to prevent it blocking other folders of the same account
  // being synced.
  if (!totalSize) return NS_ERROR_NOT_AVAILABLE;

  // ensure that we don't exceed the given size limit for this particular group
  if (aSizeLimit && aSizeLimit < totalSize) return NS_ERROR_FAILURE;

  if (!messagesToDownload.IsEmpty()) {
    rv = aAutoSyncStateObj->DownloadMessagesForOffline(messagesToDownload);

    int32_t totalCount;
    (void)aAutoSyncStateObj->GetTotalMessageCount(&totalCount);

    nsCOMPtr<nsIMsgFolder> folder;
    aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
    if (NS_SUCCEEDED(rv) && folder)
      NOTIFY_LISTENERS(OnDownloadStarted,
                       (folder, messagesToDownload.Length(), totalCount));
  }

  return rv;
}

// clang-format off
/**
 * Assuming that the download operation on the given folder has been failed at
 * least once, execute these steps:
 *  - put the auto-sync state into ready-to-download mode
 *  - rollback the message offset so we can try the same group again (unless the
 *    retry count is reached to the given limit)
 *  - if parallel model is active, wait to be resumed by the next idle
 *  - if chained model is active, search the priority queue to find a sibling to
 *    continue with.
 */
// clang-format on
nsresult nsAutoSyncManager::HandleDownloadErrorFor(
    nsIAutoSyncState* aAutoSyncStateObj, const nsresult error) {
  if (!aAutoSyncStateObj) return NS_ERROR_INVALID_ARG;

  // ensure that an error occurred
  if (NS_SUCCEEDED(error)) return NS_OK;

  // NS_ERROR_NOT_AVAILABLE is a special case/error happens when the queued
  // folder doesn't have any message to download (see bug 457342). In such case
  // we shouldn't retry the current message group, nor notify listeners. Simply
  // continuing with the next sibling in the priority queue would suffice.

  if (NS_ERROR_NOT_AVAILABLE != error) {
    // force the auto-sync state to try downloading the same group at least
    // kGroupRetryCount times before it moves to the next one
    aAutoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount);

    nsCOMPtr<nsIMsgFolder> folder;
    aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
    if (folder) NOTIFY_LISTENERS(OnDownloadError, (folder));
  }

  // if parallel model, don't do anything else

  if (mDownloadModel == dmChained) {
    // switch to the next folder in the chain and continue downloading
    nsIAutoSyncState* autoSyncStateObj = aAutoSyncStateObj;
    nsIAutoSyncState* nextAutoSyncStateObj = nullptr;
    while (
        (nextAutoSyncStateObj = GetNextSibling(mPriorityQ, autoSyncStateObj))) {
      autoSyncStateObj = nextAutoSyncStateObj;
      nsresult rv = DownloadMessagesForOffline(autoSyncStateObj);
      if (NS_SUCCEEDED(rv)) break;
      if (rv == NS_ERROR_NOT_AVAILABLE)
        // next folder in the chain also doesn't have any message to download
        // switch to next one if any
        continue;
      autoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount);
    }
  }

  return NS_OK;
}

NS_IMETHODIMP nsAutoSyncManager::GetGroupSize(uint32_t* aGroupSize) {
  NS_ENSURE_ARG_POINTER(aGroupSize);
  *aGroupSize = mGroupSize;
  return NS_OK;
}
NS_IMETHODIMP nsAutoSyncManager::SetGroupSize(uint32_t aGroupSize) {
  mGroupSize = aGroupSize ? aGroupSize : kDefaultGroupSize;
  return NS_OK;
}

NS_IMETHODIMP nsAutoSyncManager::GetMsgStrategy(
    nsIAutoSyncMsgStrategy** aMsgStrategy) {
  NS_ENSURE_ARG_POINTER(aMsgStrategy);

  // lazily create if it is not done already
  if (!mMsgStrategyImpl) {
    mMsgStrategyImpl = new nsDefaultAutoSyncMsgStrategy;
    if (!mMsgStrategyImpl) return NS_ERROR_OUT_OF_MEMORY;
  }

  NS_IF_ADDREF(*aMsgStrategy = mMsgStrategyImpl);
  return NS_OK;
}
NS_IMETHODIMP nsAutoSyncManager::SetMsgStrategy(
    nsIAutoSyncMsgStrategy* aMsgStrategy) {
  mMsgStrategyImpl = aMsgStrategy;
  return NS_OK;
}

NS_IMETHODIMP nsAutoSyncManager::GetFolderStrategy(
    nsIAutoSyncFolderStrategy** aFolderStrategy) {
  NS_ENSURE_ARG_POINTER(aFolderStrategy);

  // lazily create if it is not done already
  if (!mFolderStrategyImpl) {
    mFolderStrategyImpl = new nsDefaultAutoSyncFolderStrategy;
    if (!mFolderStrategyImpl) return NS_ERROR_OUT_OF_MEMORY;
  }

  NS_IF_ADDREF(*aFolderStrategy = mFolderStrategyImpl);
  return NS_OK;
}
NS_IMETHODIMP nsAutoSyncManager::SetFolderStrategy(
    nsIAutoSyncFolderStrategy* aFolderStrategy) {
  mFolderStrategyImpl = aFolderStrategy;
  return NS_OK;
}

NS_IMETHODIMP
nsAutoSyncManager::DoesMsgFitDownloadCriteria(nsIMsgDBHdr* aMsgHdr,
                                              bool* aResult) {
  NS_ENSURE_ARG_POINTER(aResult);

  uint32_t msgFlags = 0;
  aMsgHdr->GetFlags(&msgFlags);

  // check whether this message is marked imap deleted or not
  *aResult = !(msgFlags & nsMsgMessageFlags::IMAPDeleted);
  if (!(*aResult)) return NS_OK;

  bool shouldStoreMsgOffline = true;
  nsCOMPtr<nsIMsgFolder> folder;
  aMsgHdr->GetFolder(getter_AddRefs(folder));
  if (folder) {
    nsMsgKey msgKey;
    nsresult rv = aMsgHdr->GetMessageKey(&msgKey);
    // a cheap way to get the size limit for this folder and make
    // sure that we don't have this message offline already
    if (NS_SUCCEEDED(rv))
      folder->ShouldStoreMsgOffline(msgKey, &shouldStoreMsgOffline);
  }

  *aResult &= shouldStoreMsgOffline;

  return NS_OK;
}

NS_IMETHODIMP nsAutoSyncManager::OnDownloadQChanged(
    nsIAutoSyncState* aAutoSyncStateObj) {
  nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(aAutoSyncStateObj);
  if (!autoSyncStateObj) return NS_ERROR_INVALID_ARG;

  if (mPaused) return NS_OK;
  // We want to start downloading immediately unless the folder is excluded.
  bool excluded = false;
  nsCOMPtr<nsIAutoSyncFolderStrategy> folStrategy;
  nsCOMPtr<nsIMsgFolder> folder;

  GetFolderStrategy(getter_AddRefs(folStrategy));
  autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));

  if (folder && folStrategy) folStrategy->IsExcluded(folder, &excluded);

  nsresult rv = NS_OK;

  if (!excluded) {
    // Add this folder into the priority queue.
    autoSyncStateObj->SetState(nsAutoSyncState::stReadyToDownload);
    ScheduleFolderForOfflineDownload(autoSyncStateObj);

    // If we operate in parallel mode or if there is no sibling downloading
    // messages at the moment, we can download the first group of the messages
    // for this folder
    if (mDownloadModel == dmParallel ||
        !DoesQContainAnySiblingOf(mPriorityQ, autoSyncStateObj,
                                  nsAutoSyncState::stDownloadInProgress)) {
      // this will download the first group of messages immediately;
      // to ensure that we don't end up downloading a large single message in
      // not-idle time, we enforce a limit. If there is no message fits into
      // this limit we postpone the download until the next idle.
      if (GetIdleState() == notIdle)
        rv = DownloadMessagesForOffline(autoSyncStateObj, kFirstGroupSizeLimit);
      else
        rv = DownloadMessagesForOffline(autoSyncStateObj);

      if (NS_FAILED(rv))
        autoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount);
    }
  }
  return rv;
}

NS_IMETHODIMP
nsAutoSyncManager::OnDownloadStarted(nsIAutoSyncState* aAutoSyncStateObj,
                                     nsresult aStartCode) {
  nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(aAutoSyncStateObj);
  if (!autoSyncStateObj) return NS_ERROR_INVALID_ARG;

  // resume downloads during next idle time
  if (NS_FAILED(aStartCode))
    autoSyncStateObj->SetState(nsAutoSyncState::stReadyToDownload);

  return aStartCode;
}

NS_IMETHODIMP
nsAutoSyncManager::OnDownloadCompleted(nsIAutoSyncState* aAutoSyncStateObj,
                                       nsresult aExitCode) {
  nsCOMPtr<nsIAutoSyncState> autoSyncStateObj(aAutoSyncStateObj);
  if (!autoSyncStateObj) return NS_ERROR_INVALID_ARG;

  nsresult rv = aExitCode;

  if (NS_FAILED(aExitCode)) {
    // retry the same group kGroupRetryCount times
    // try again if TB still idle, otherwise wait for the next idle time
    autoSyncStateObj->TryCurrentGroupAgain(kGroupRetryCount);
    if (GetIdleState() != notIdle) {
      rv = DownloadMessagesForOffline(autoSyncStateObj);
      if (NS_FAILED(rv)) rv = HandleDownloadErrorFor(autoSyncStateObj, rv);
    }
    return rv;
  }

  // download is successful, reset the retry counter of the folder
  autoSyncStateObj->ResetRetryCounter();

  nsCOMPtr<nsIMsgFolder> folder;
  aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
  if (folder) NOTIFY_LISTENERS(OnDownloadCompleted, (folder));

  int32_t count;
  rv = autoSyncStateObj->GetPendingMessageCount(&count);
  NS_ENSURE_SUCCESS(rv, rv);

  nsIAutoSyncState* nextFolderToDownload = nullptr;
  if (count > 0) {
    autoSyncStateObj->SetState(nsAutoSyncState::stReadyToDownload);

    // in parallel model, we continue downloading the same folder as long as it
    // has more pending messages
    nextFolderToDownload = autoSyncStateObj;

    // in chained model, ensure that we are always downloading the highest
    // priority folder first
    if (mDownloadModel == dmChained) {
      // switch to higher priority folder and continue to download,
      // if any added recently
      int32_t myIndex = mPriorityQ.IndexOf(autoSyncStateObj);

      int32_t siblingIndex;
      nsIAutoSyncState* sibling =
          GetHighestPrioSibling(mPriorityQ, autoSyncStateObj, &siblingIndex);

      // lesser index = higher priority
      if (sibling && myIndex > -1 && siblingIndex < myIndex)
        nextFolderToDownload = sibling;
    }
  } else {
    autoSyncStateObj->SetState(nsAutoSyncState::stCompletedIdle);

    nsCOMPtr<nsIMsgFolder> folder;
    nsresult rv = autoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));

    if (NS_SUCCEEDED(rv) && mPriorityQ.RemoveObject(autoSyncStateObj))
      NOTIFY_LISTENERS(OnFolderRemovedFromQ,
                       (nsIAutoSyncMgrListener::PriorityQueue, folder));

    // find the next folder owned by the same server in the queue and continue
    // downloading
    if (mDownloadModel == dmChained)
      nextFolderToDownload =
          GetHighestPrioSibling(mPriorityQ, autoSyncStateObj);

  }  // endif

  // continue downloading if TB is still in idle state
  if (nextFolderToDownload && GetIdleState() != notIdle) {
    rv = DownloadMessagesForOffline(nextFolderToDownload);
    if (NS_FAILED(rv)) rv = HandleDownloadErrorFor(nextFolderToDownload, rv);
  }

  return rv;
}

NS_IMETHODIMP nsAutoSyncManager::GetDownloadModel(int32_t* aDownloadModel) {
  NS_ENSURE_ARG_POINTER(aDownloadModel);
  *aDownloadModel = mDownloadModel;
  return NS_OK;
}
NS_IMETHODIMP nsAutoSyncManager::SetDownloadModel(int32_t aDownloadModel) {
  mDownloadModel = aDownloadModel;
  return NS_OK;
}

NS_IMETHODIMP nsAutoSyncManager::AddListener(
    nsIAutoSyncMgrListener* aListener) {
  NS_ENSURE_ARG_POINTER(aListener);
  mListeners.AppendElementUnlessExists(aListener);
  return NS_OK;
}

NS_IMETHODIMP nsAutoSyncManager::RemoveListener(
    nsIAutoSyncMgrListener* aListener) {
  NS_ENSURE_ARG_POINTER(aListener);
  mListeners.RemoveElement(aListener);
  return NS_OK;
}

/* readonly attribute unsigned long discoveryQLength; */
NS_IMETHODIMP nsAutoSyncManager::GetDiscoveryQLength(
    uint32_t* aDiscoveryQLength) {
  NS_ENSURE_ARG_POINTER(aDiscoveryQLength);
  *aDiscoveryQLength = mDiscoveryQ.Count();
  return NS_OK;
}

/* readonly attribute unsigned long uploadQLength; */
NS_IMETHODIMP nsAutoSyncManager::GetUpdateQLength(uint32_t* aUpdateQLength) {
  NS_ENSURE_ARG_POINTER(aUpdateQLength);
  *aUpdateQLength = mUpdateQ.Count();
  return NS_OK;
}

/* readonly attribute unsigned long downloadQLength; */
NS_IMETHODIMP nsAutoSyncManager::GetDownloadQLength(
    uint32_t* aDownloadQLength) {
  NS_ENSURE_ARG_POINTER(aDownloadQLength);
  *aDownloadQLength = mPriorityQ.Count();
  return NS_OK;
}

NS_IMETHODIMP
nsAutoSyncManager::OnFolderHasPendingMsgs(nsIAutoSyncState* aAutoSyncStateObj) {
  NS_ENSURE_ARG_POINTER(aAutoSyncStateObj);
  if (mUpdateQ.IndexOf(aAutoSyncStateObj) == -1) {
    nsCOMPtr<nsIMsgFolder> folder;
    aAutoSyncStateObj->GetOwnerFolder(getter_AddRefs(folder));
    // If this folder isn't the trash, add it to the update q.
    if (folder) {
      bool isTrash;
      folder->GetFlag(nsMsgFolderFlags::Trash, &isTrash);
      if (!isTrash) {
        bool isSentOrArchive;
        folder->IsSpecialFolder(
            nsMsgFolderFlags::SentMail | nsMsgFolderFlags::Archive, true,
            &isSentOrArchive);
        // Sent or archive folders go to the q front, the rest to the end.
        if (isSentOrArchive)
          mUpdateQ.InsertObjectAt(aAutoSyncStateObj, 0);
        else
          mUpdateQ.AppendObject(aAutoSyncStateObj);
        aAutoSyncStateObj->SetState(nsAutoSyncState::stUpdateNeeded);
        NOTIFY_LISTENERS(OnFolderAddedIntoQ,
                         (nsIAutoSyncMgrListener::UpdateQueue, folder));
      }
    }
  }
  return NS_OK;
}

void nsAutoSyncManager::SetIdleState(IdleState st) { mIdleState = st; }

nsAutoSyncManager::IdleState nsAutoSyncManager::GetIdleState() const {
  return mIdleState;
}

NS_IMPL_ISUPPORTS(nsAutoSyncManager, nsIObserver, nsIUrlListener,
                  nsIAutoSyncManager)
