/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "msgCore.h"
#include "nntpCore.h"
#include "netCore.h"
#include "nsIMsgNewsFolder.h"
#include "nsIStringBundle.h"
#include "nsNewsDownloader.h"
#include "nsINntpService.h"
#include "nsMsgNewsCID.h"
#include "nsIMsgSearchSession.h"
#include "nsIMsgSearchTerm.h"
#include "nsIMsgSearchValidityManager.h"
#include "nsRDFCID.h"
#include "nsIMsgAccountManager.h"
#include "nsMsgFolderFlags.h"
#include "nsIRequestObserver.h"
#include "nsIMsgMailSession.h"
#include "nsMsgMessageFlags.h"
#include "nsIMsgStatusFeedback.h"
#include "nsServiceManagerUtils.h"
#include "nsComponentManagerUtils.h"
#include "nsMsgUtils.h"
#include "mozilla/Services.h"
#include "nsIArray.h"
#include "nsArrayUtils.h"

// This file contains the news article download state machine.

// if pIds is not null, download the articles whose id's are passed in. Otherwise,
// which articles to download is determined by nsNewsDownloader object,
// or subclasses thereof. News can download marked objects, for example.
nsresult nsNewsDownloader::DownloadArticles(nsIMsgWindow *window, nsIMsgFolder *folder, nsTArray<nsMsgKey> *pIds)
{
  if (pIds != nullptr)
    m_keysToDownload.InsertElementsAt(0, pIds->Elements(), pIds->Length());

  if (!m_keysToDownload.IsEmpty())
    m_downloadFromKeys = true;

  m_folder = folder;
  m_window = window;
  m_numwrote = 0;

  bool headersToDownload = GetNextHdrToRetrieve();
  // should we have a special error code for failure here?
  return (headersToDownload) ? DownloadNext(true) : NS_ERROR_FAILURE;
}

/* Saving news messages
 */

NS_IMPL_ISUPPORTS(nsNewsDownloader, nsIUrlListener, nsIMsgSearchNotify)

nsNewsDownloader::nsNewsDownloader(nsIMsgWindow *window, nsIMsgDatabase *msgDB, nsIUrlListener *listener)
{
  m_numwrote = 0;
  m_downloadFromKeys = false;
  m_newsDB = msgDB;
  m_abort = false;
  m_listener = listener;
  m_window = window;
  m_lastPercent = -1;
  m_lastProgressTime = 0;
  // not the perfect place for this, but I think it will work.
  if (m_window)
    m_window->SetStopped(false);
}

nsNewsDownloader::~nsNewsDownloader()
{
  if (m_listener)
    m_listener->OnStopRunningUrl(/* don't have a url */nullptr, m_status);
  if (m_newsDB)
  {
    m_newsDB->Commit(nsMsgDBCommitType::kLargeCommit);
    m_newsDB = nullptr;
  }
}

NS_IMETHODIMP nsNewsDownloader::OnStartRunningUrl(nsIURI* url)
{
  return NS_OK;
}

NS_IMETHODIMP nsNewsDownloader::OnStopRunningUrl(nsIURI* url, nsresult exitCode)
{
  bool stopped = false;
  if (m_window)
    m_window->GetStopped(&stopped);
  if (stopped)
    exitCode = NS_BINDING_ABORTED;

 nsresult rv = exitCode;
  if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND)
    rv = DownloadNext(false);

  return rv;
}

nsresult nsNewsDownloader::DownloadNext(bool firstTimeP)
{
  nsresult rv;
  if (!firstTimeP)
  {
    bool moreHeaders = GetNextHdrToRetrieve();
    if (!moreHeaders)
    {
      if (m_listener)
        m_listener->OnStopRunningUrl(nullptr, NS_OK);
      return NS_OK;
    }
  }
  StartDownload();
  m_wroteAnyP = false;
  nsCOMPtr <nsINntpService> nntpService = do_GetService(NS_NNTPSERVICE_CONTRACTID,&rv);
  NS_ENSURE_SUCCESS(rv, rv);

  return nntpService->FetchMessage(m_folder, m_keyToDownload, m_window, nullptr, this, nullptr);
}

bool DownloadNewsArticlesToOfflineStore::GetNextHdrToRetrieve()
{
  nsresult rv;

  if (m_downloadFromKeys)
    return nsNewsDownloader::GetNextHdrToRetrieve();

  if (m_headerEnumerator == nullptr)
    rv = m_newsDB->EnumerateMessages(getter_AddRefs(m_headerEnumerator));

  bool hasMore = false;

  while (NS_SUCCEEDED(rv = m_headerEnumerator->HasMoreElements(&hasMore)) && hasMore)
  {
    nsCOMPtr <nsISupports> supports;
    rv = m_headerEnumerator->GetNext(getter_AddRefs(supports));
    m_newsHeader = do_QueryInterface(supports);
    NS_ENSURE_SUCCESS(rv, false);
    uint32_t hdrFlags;
    m_newsHeader->GetFlags(&hdrFlags);
    if (hdrFlags & nsMsgMessageFlags::Marked)
    {
      m_newsHeader->GetMessageKey(&m_keyToDownload);
      break;
    }
    else
    {
      m_newsHeader = nullptr;
    }
  }
  return hasMore;
}

void nsNewsDownloader::Abort() {}
void nsNewsDownloader::Complete() {}

bool nsNewsDownloader::GetNextHdrToRetrieve()
{
  nsresult rv;
  if (m_downloadFromKeys)
  {
    if (m_numwrote >= (int32_t) m_keysToDownload.Length())
      return false;

    m_keyToDownload = m_keysToDownload[m_numwrote++];
    int32_t percent;
    percent = (100 * m_numwrote) / (int32_t) m_keysToDownload.Length();

    int64_t nowMS = 0;
    if (percent < 100)  // always need to do 100%
    {
      nowMS = PR_IntervalToMilliseconds(PR_IntervalNow());
      if (nowMS - m_lastProgressTime < 750)
        return true;
    }

    m_lastProgressTime = nowMS;
    nsCOMPtr <nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_folder);
    nsCOMPtr<nsIStringBundleService> bundleService =
      mozilla::services::GetStringBundleService();
    NS_ENSURE_TRUE(bundleService, false);
    nsCOMPtr<nsIStringBundle> bundle;
    rv = bundleService->CreateBundle(NEWS_MSGS_URL, getter_AddRefs(bundle));
    NS_ENSURE_SUCCESS(rv, false);

    nsAutoString firstStr;
    firstStr.AppendInt(m_numwrote);
    nsAutoString totalStr;
    totalStr.AppendInt(int(m_keysToDownload.Length()));
    nsString prettiestName;
    nsString statusString;

    m_folder->GetPrettiestName(prettiestName);

    const char16_t *formatStrings[3] = { firstStr.get(), totalStr.get(), prettiestName.get() };
    rv = bundle->FormatStringFromName(u"downloadingArticlesForOffline",
                                      formatStrings, 3, getter_Copies(statusString));
    NS_ENSURE_SUCCESS(rv, false);
    ShowProgress(statusString.get(), percent);
    return true;
  }
  NS_ASSERTION(false, "shouldn't get here if we're not downloading from keys.");
  return false;  // shouldn't get here if we're not downloading from keys.
}

nsresult nsNewsDownloader::ShowProgress(const char16_t *progressString, int32_t percent)
{
  if (!m_statusFeedback)
  {
    if (m_window)
      m_window->GetStatusFeedback(getter_AddRefs(m_statusFeedback));
  }
  if (m_statusFeedback)
  {
    m_statusFeedback->ShowStatusString(nsDependentString(progressString));
    if (percent != m_lastPercent)
    {
      m_statusFeedback->ShowProgress(percent);
      m_lastPercent = percent;
    }
  }
  return NS_OK;
}

NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStartRunningUrl(nsIURI* url)
{
  return NS_OK;
}


NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStopRunningUrl(nsIURI* url, nsresult exitCode)
{
  m_status = exitCode;
  if (m_newsHeader != nullptr)
  {
#ifdef DEBUG_bienvenu
    //    XP_Trace("finished retrieving %ld\n", m_newsHeader->GetMessageKey());
#endif
    if (m_newsDB)
    {
      nsMsgKey msgKey;
      m_newsHeader->GetMessageKey(&msgKey);
      m_newsDB->MarkMarked(msgKey, false, nullptr);
    }
  }
  m_newsHeader = nullptr;
  return nsNewsDownloader::OnStopRunningUrl(url, exitCode);
}

int DownloadNewsArticlesToOfflineStore::FinishDownload()
{
  return 0;
}


NS_IMETHODIMP nsNewsDownloader::OnSearchHit(nsIMsgDBHdr *header, nsIMsgFolder *folder)
{
  NS_ENSURE_ARG(header);


  uint32_t msgFlags;
  header->GetFlags(&msgFlags);
  // only need to download articles we don't already have...
  if (! (msgFlags & nsMsgMessageFlags::Offline))
  {
    nsMsgKey key;
    header->GetMessageKey(&key);
    m_keysToDownload.AppendElement(key);
  }
  return NS_OK;
}

NS_IMETHODIMP nsNewsDownloader::OnSearchDone(nsresult status)
{
  if (m_keysToDownload.IsEmpty())
  {
    if (m_listener)
      return m_listener->OnStopRunningUrl(nullptr, NS_OK);
  }
  nsresult rv = DownloadArticles(m_window, m_folder,
                  /* we've already set m_keysToDownload, so don't pass it in */ nullptr);
  if (NS_FAILED(rv))
    if (m_listener)
      m_listener->OnStopRunningUrl(nullptr, rv);

  return rv;
}
NS_IMETHODIMP nsNewsDownloader::OnNewSearch()
{
  return NS_OK;
}

int DownloadNewsArticlesToOfflineStore::StartDownload()
{
  m_newsDB->GetMsgHdrForKey(m_keyToDownload, getter_AddRefs(m_newsHeader));
  return 0;
}

DownloadNewsArticlesToOfflineStore::DownloadNewsArticlesToOfflineStore(nsIMsgWindow *window, nsIMsgDatabase *db, nsIUrlListener *listener)
  : nsNewsDownloader(window, db, listener)
{
  m_newsDB = db;
}

DownloadNewsArticlesToOfflineStore::~DownloadNewsArticlesToOfflineStore()
{
}

DownloadMatchingNewsArticlesToNewsDB::DownloadMatchingNewsArticlesToNewsDB
  (nsIMsgWindow *window, nsIMsgFolder *folder, nsIMsgDatabase *newsDB,
   nsIUrlListener *listener) :
   DownloadNewsArticlesToOfflineStore(window, newsDB, listener)
{
  m_window = window;
  m_folder = folder;
  m_newsDB = newsDB;
  m_downloadFromKeys = true;  // search term matching means downloadFromKeys.
}

DownloadMatchingNewsArticlesToNewsDB::~DownloadMatchingNewsArticlesToNewsDB()
{
}


NS_IMPL_ISUPPORTS(nsMsgDownloadAllNewsgroups, nsIUrlListener)


nsMsgDownloadAllNewsgroups::nsMsgDownloadAllNewsgroups(nsIMsgWindow *window, nsIUrlListener *listener)
{
  m_window = window;
  m_listener = listener;
  m_downloaderForGroup = new DownloadMatchingNewsArticlesToNewsDB(window, nullptr, nullptr, this);
  NS_IF_ADDREF(m_downloaderForGroup);
  m_downloadedHdrsForCurGroup = false;
}

nsMsgDownloadAllNewsgroups::~nsMsgDownloadAllNewsgroups()
{
  NS_IF_RELEASE(m_downloaderForGroup);
}

NS_IMETHODIMP nsMsgDownloadAllNewsgroups::OnStartRunningUrl(nsIURI* url)
{
    return NS_OK;
}

NS_IMETHODIMP
nsMsgDownloadAllNewsgroups::OnStopRunningUrl(nsIURI* url, nsresult exitCode)
{
  nsresult rv = exitCode;
  if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND)
  {
    if (m_downloadedHdrsForCurGroup)
    {
      bool savingArticlesOffline = false;
      nsCOMPtr <nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
      if (newsFolder)
        newsFolder->GetSaveArticleOffline(&savingArticlesOffline);

      m_downloadedHdrsForCurGroup = false;
      if (savingArticlesOffline) // skip this group - we're saving to it already
        rv = ProcessNextGroup();
      else
        rv = DownloadMsgsForCurrentGroup();
    }
    else
    {
      rv = ProcessNextGroup();
    }
  }
  else if (m_listener)  // notify main observer.
    m_listener->OnStopRunningUrl(url, exitCode);

  return rv;
}

/**
 * Leaves m_currentServer at the next nntp "server" that
 * might have folders to download for offline use. If no more servers,
 * m_currentServer will be left at nullptr and the function returns false.
 * Also, sets up m_serverEnumerator to enumerate over the server.
 * If no servers found, m_serverEnumerator will be left at null.
 */
bool nsMsgDownloadAllNewsgroups::AdvanceToNextServer()
{
  nsresult rv;

  if (!m_allServers)
  {
    nsCOMPtr<nsIMsgAccountManager> accountManager =
             do_GetService(NS_MSGACCOUNTMANAGER_CONTRACTID, &rv);
    NS_ASSERTION(accountManager && NS_SUCCEEDED(rv), "couldn't get account mgr");
    if (!accountManager || NS_FAILED(rv))
      return false;

    rv = accountManager->GetAllServers(getter_AddRefs(m_allServers));
    NS_ENSURE_SUCCESS(rv, false);
  }
  uint32_t serverIndex = 0;
  if (m_currentServer)
  {
    rv = m_allServers->IndexOf(0, m_currentServer, &serverIndex);
    if (NS_FAILED(rv))
      serverIndex = -1;

    ++serverIndex;
  }
  m_currentServer = nullptr;
  uint32_t numServers;
  m_allServers->GetLength(&numServers);
  nsCOMPtr <nsIMsgFolder> rootFolder;

  while (serverIndex < numServers)
  {
    nsCOMPtr<nsIMsgIncomingServer> server = do_QueryElementAt(m_allServers, serverIndex);
    serverIndex++;

    nsCOMPtr <nsINntpIncomingServer> newsServer = do_QueryInterface(server);
    if (!newsServer) // we're only looking for news servers
      continue;

    if (server)
    {
      m_currentServer = server;
      server->GetRootFolder(getter_AddRefs(rootFolder));
      if (rootFolder)
      {
        rv = rootFolder->GetDescendants(getter_AddRefs(m_allFolders));
        if (NS_SUCCEEDED(rv))
        {
          rv = m_allFolders->Enumerate(getter_AddRefs(m_serverEnumerator));
          if (NS_SUCCEEDED(rv) && m_serverEnumerator)
          {
            bool hasMore = false;
            rv = m_serverEnumerator->HasMoreElements(&hasMore);
            if (NS_SUCCEEDED(rv) && hasMore)
              return true;
          }
        }
      }
    }
  }
  return false;
}

/**
 * Sets m_currentFolder to the next usable folder.
 *
 * @return  False if no more folders found, otherwise true.
 */
bool nsMsgDownloadAllNewsgroups::AdvanceToNextGroup()
{
  nsresult rv = NS_OK;

  if (m_currentFolder)
  {
    nsCOMPtr <nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
    if (newsFolder)
      newsFolder->SetSaveArticleOffline(false);

    nsCOMPtr<nsIMsgMailSession> session =
             do_GetService(NS_MSGMAILSESSION_CONTRACTID, &rv);
    if (NS_SUCCEEDED(rv) && session)
    {
      bool folderOpen;
      uint32_t folderFlags;
      m_currentFolder->GetFlags(&folderFlags);
      session->IsFolderOpenInWindow(m_currentFolder, &folderOpen);
      if (!folderOpen && ! (folderFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox)))
        m_currentFolder->SetMsgDatabase(nullptr);
    }
    m_currentFolder = nullptr;
  }

  bool hasMore = false;
  if (m_currentServer)
    m_serverEnumerator->HasMoreElements(&hasMore);
  if (!hasMore)
    hasMore = AdvanceToNextServer();

  if (hasMore)
  {
    nsCOMPtr<nsISupports> supports;
    rv = m_serverEnumerator->GetNext(getter_AddRefs(supports));
    if (NS_SUCCEEDED(rv))
      m_currentFolder = do_QueryInterface(supports);
  }
  return m_currentFolder;
}

nsresult DownloadMatchingNewsArticlesToNewsDB::RunSearch(nsIMsgFolder *folder, nsIMsgDatabase *newsDB, nsIMsgSearchSession *searchSession)
{
  m_folder = folder;
  m_newsDB = newsDB;
  m_searchSession = searchSession;

  m_keysToDownload.Clear();

  NS_ENSURE_ARG(searchSession);
  NS_ENSURE_ARG(folder);

  searchSession->RegisterListener(this,
                                  nsIMsgSearchSession::allNotifications);
  nsresult rv = searchSession->AddScopeTerm(nsMsgSearchScope::localNews, folder);
  NS_ENSURE_SUCCESS(rv, rv);

  return searchSession->Search(m_window);
}

nsresult nsMsgDownloadAllNewsgroups::ProcessNextGroup()
{
  bool done = false;

  while (!done)
  {
    done = !AdvanceToNextGroup();
    if (!done && m_currentFolder)
    {
      uint32_t folderFlags;
      m_currentFolder->GetFlags(&folderFlags);
      if (folderFlags & nsMsgFolderFlags::Offline)
        break;
    }
  }
  if (done)
  {
    if (m_listener)
      return m_listener->OnStopRunningUrl(nullptr, NS_OK);
  }
  m_downloadedHdrsForCurGroup = true;
  return m_currentFolder ? m_currentFolder->GetNewMessages(m_window, this) : NS_ERROR_NOT_INITIALIZED;
}

nsresult nsMsgDownloadAllNewsgroups::DownloadMsgsForCurrentGroup()
{
  NS_ENSURE_TRUE(m_downloaderForGroup, NS_ERROR_OUT_OF_MEMORY);
  nsCOMPtr <nsIMsgDatabase> db;
  nsCOMPtr <nsIMsgDownloadSettings> downloadSettings;
  m_currentFolder->GetMsgDatabase(getter_AddRefs(db));
  nsresult rv = m_currentFolder->GetDownloadSettings(getter_AddRefs(downloadSettings));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr <nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
  if (newsFolder)
    newsFolder->SetSaveArticleOffline(true);

  nsCOMPtr <nsIMsgSearchSession> searchSession = do_CreateInstance(NS_MSGSEARCHSESSION_CONTRACTID, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  bool downloadByDate, downloadUnreadOnly;
  uint32_t ageLimitOfMsgsToDownload;

  downloadSettings->GetDownloadByDate(&downloadByDate);
  downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly);
  downloadSettings->GetAgeLimitOfMsgsToDownload(&ageLimitOfMsgsToDownload);

  nsCOMPtr <nsIMsgSearchTerm> term;
  nsCOMPtr <nsIMsgSearchValue>    value;

  rv = searchSession->CreateTerm(getter_AddRefs(term));
  NS_ENSURE_SUCCESS(rv, rv);
  term->GetValue(getter_AddRefs(value));

  if (downloadUnreadOnly)
  {
    value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
    value->SetStatus(nsMsgMessageFlags::Read);
    searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, value, true, nullptr);
  }
  if (downloadByDate)
  {
    value->SetAttrib(nsMsgSearchAttrib::AgeInDays);
    value->SetAge(ageLimitOfMsgsToDownload);
    searchSession->AddSearchTerm(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, value, nsMsgSearchBooleanOp::BooleanAND, nullptr);
  }
  value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
  value->SetStatus(nsMsgMessageFlags::Offline);
  searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, value, nsMsgSearchBooleanOp::BooleanAND, nullptr);

  m_downloaderForGroup->RunSearch(m_currentFolder, db, searchSession);
  return rv;
}