/* -*- Mode: C++; tab-width: 2; 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 "nsIMsgHdr.h"
#include "nsLocalUndoTxn.h"
#include "nsImapCore.h"
#include "nsIImapService.h"
#include "nsIUrlListener.h"
#include "nsIMsgLocalMailFolder.h"
#include "nsIMsgMailSession.h"
#include "nsIMsgFolderNotificationService.h"
#include "nsThreadUtils.h"
#include "nsIMsgDatabase.h"
#include "nsIMsgHdr.h"
#include "nsServiceManagerUtils.h"
#include "nsComponentManagerUtils.h"
#include "nsMsgUtils.h"
#include "nsMsgDBFolder.h"

NS_IMPL_ISUPPORTS_INHERITED(nsLocalMoveCopyMsgTxn, nsMsgTxn, nsIFolderListener)

nsLocalMoveCopyMsgTxn::nsLocalMoveCopyMsgTxn()
    : m_srcIsImap4(false), m_canUndelete(false) {}

nsLocalMoveCopyMsgTxn::~nsLocalMoveCopyMsgTxn() {}

nsresult nsLocalMoveCopyMsgTxn::Init(nsIMsgFolder* srcFolder,
                                     nsIMsgFolder* dstFolder, bool isMove) {
  nsresult rv;
  rv = SetSrcFolder(srcFolder);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = SetDstFolder(dstFolder);
  NS_ENSURE_SUCCESS(rv, rv);
  m_isMove = isMove;

  mUndoFolderListener = nullptr;

  nsCString protocolType;
  rv = srcFolder->GetURI(protocolType);
  protocolType.SetLength(protocolType.FindChar(':'));
  if (protocolType.LowerCaseEqualsLiteral("imap")) m_srcIsImap4 = true;
  return nsMsgTxn::Init();
}
nsresult nsLocalMoveCopyMsgTxn::GetSrcIsImap(bool* isImap) {
  *isImap = m_srcIsImap4;
  return NS_OK;
}
nsresult nsLocalMoveCopyMsgTxn::SetSrcFolder(nsIMsgFolder* srcFolder) {
  nsresult rv = NS_ERROR_NULL_POINTER;
  if (srcFolder) m_srcFolder = do_GetWeakReference(srcFolder, &rv);
  return rv;
}

nsresult nsLocalMoveCopyMsgTxn::SetDstFolder(nsIMsgFolder* dstFolder) {
  nsresult rv = NS_ERROR_NULL_POINTER;
  if (dstFolder) m_dstFolder = do_GetWeakReference(dstFolder, &rv);
  return rv;
}

nsresult nsLocalMoveCopyMsgTxn::AddSrcKey(nsMsgKey aKey) {
  m_srcKeyArray.AppendElement(aKey);
  return NS_OK;
}

nsresult nsLocalMoveCopyMsgTxn::AddDstKey(nsMsgKey aKey) {
  m_dstKeyArray.AppendElement(aKey);
  return NS_OK;
}

nsresult nsLocalMoveCopyMsgTxn::AddDstMsgSize(uint32_t msgSize) {
  m_dstSizeArray.AppendElement(msgSize);
  return NS_OK;
}

nsresult nsLocalMoveCopyMsgTxn::UndoImapDeleteFlag(nsIMsgFolder* folder,
                                                   nsTArray<nsMsgKey>& keyArray,
                                                   bool deleteFlag) {
  nsresult rv = NS_ERROR_FAILURE;
  if (m_srcIsImap4) {
    nsCOMPtr<nsIImapService> imapService =
        do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);
    nsCOMPtr<nsIUrlListener> urlListener;
    nsCString msgIds;
    uint32_t i, count = keyArray.Length();
    urlListener = do_QueryInterface(folder, &rv);
    for (i = 0; i < count; i++) {
      if (!msgIds.IsEmpty()) msgIds.Append(',');
      msgIds.AppendInt((int32_t)keyArray[i]);
    }
    // This is to make sure that we are in the selected state
    // when executing the imap url; we don't want to load the
    // folder so use lite select to do the trick
    rv = imapService->LiteSelectFolder(folder, urlListener, nullptr, nullptr);
    if (!deleteFlag)
      rv = imapService->AddMessageFlags(folder, urlListener, msgIds,
                                        kImapMsgDeletedFlag, true);
    else
      rv = imapService->SubtractMessageFlags(folder, urlListener, msgIds,
                                             kImapMsgDeletedFlag, true);
    if (NS_SUCCEEDED(rv) && m_msgWindow) folder->UpdateFolder(m_msgWindow);
    rv = NS_OK;  // always return NS_OK to indicate that the src is imap
  } else
    rv = NS_ERROR_FAILURE;
  return rv;
}

NS_IMETHODIMP
nsLocalMoveCopyMsgTxn::UndoTransaction() {
  nsresult rv;
  nsCOMPtr<nsIMsgDatabase> dstDB;

  nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  nsCOMPtr<nsIMsgLocalMailFolder> dstlocalMailFolder =
      do_QueryReferent(m_dstFolder, &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  dstlocalMailFolder->GetDatabaseWOReparse(getter_AddRefs(dstDB));

  if (!dstDB) {
    // This will listen for the db reparse finishing, and the corresponding
    // FolderLoadedNotification. When it gets that, it will then call
    // UndoTransactionInternal.
    mUndoFolderListener = new nsLocalUndoFolderListener(this, dstFolder);
    if (!mUndoFolderListener) return NS_ERROR_OUT_OF_MEMORY;
    NS_ADDREF(mUndoFolderListener);

    nsCOMPtr<nsIMsgMailSession> mailSession =
        do_GetService("@mozilla.org/messenger/services/session;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mailSession->AddFolderListener(mUndoFolderListener,
                                        nsIFolderListener::event);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = dstFolder->GetMsgDatabase(getter_AddRefs(dstDB));
    NS_ENSURE_SUCCESS(rv, rv);
  } else
    rv = UndoTransactionInternal();
  return rv;
}

nsresult nsLocalMoveCopyMsgTxn::UndoTransactionInternal() {
  nsresult rv = NS_ERROR_FAILURE;

  if (mUndoFolderListener) {
    nsCOMPtr<nsIMsgMailSession> mailSession =
        do_GetService("@mozilla.org/messenger/services/session;1", &rv);
    NS_ENSURE_SUCCESS(rv, rv);

    rv = mailSession->RemoveFolderListener(mUndoFolderListener);
    NS_ENSURE_SUCCESS(rv, rv);

    NS_RELEASE(mUndoFolderListener);
    mUndoFolderListener = nullptr;
  }

  nsCOMPtr<nsIMsgDatabase> srcDB;
  nsCOMPtr<nsIMsgDatabase> dstDB;
  nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
  if (NS_FAILED(rv)) return rv;

  rv = dstFolder->GetMsgDatabase(getter_AddRefs(dstDB));
  if (NS_FAILED(rv)) return rv;

  uint32_t count = m_srcKeyArray.Length();
  uint32_t i;

  // protect against a bogus undo txn without any source keys
  // see bug #179856 for details
  NS_ASSERTION(count, "no source keys");
  if (!count) return NS_ERROR_UNEXPECTED;

  if (m_isMove) {
    if (m_srcIsImap4) {
      bool deleteFlag =
          true;  // message has been deleted -we are trying to undo it
      CheckForToggleDelete(srcFolder, m_srcKeyArray[0],
                           &deleteFlag);  // there could have been a toggle.
      rv = UndoImapDeleteFlag(srcFolder, m_srcKeyArray, deleteFlag);
    } else if (m_canUndelete) {
      nsTArray<RefPtr<nsIMsgDBHdr>> srcMessages(count);
      nsTArray<RefPtr<nsIMsgDBHdr>> destMessages(count);

      for (i = 0; i < count; i++) {
        nsCOMPtr<nsIMsgDBHdr> oldHdr;
        rv = dstDB->GetMsgHdrForKey(m_dstKeyArray[i], getter_AddRefs(oldHdr));
        NS_ASSERTION(oldHdr, "fatal ... cannot get old msg header");
        if (NS_SUCCEEDED(rv) && oldHdr) {
          nsCOMPtr<nsIMsgDBHdr> newHdr;
          rv = srcDB->CopyHdrFromExistingHdr(m_srcKeyArray[i], oldHdr, true,
                                             getter_AddRefs(newHdr));
          NS_ASSERTION(newHdr, "fatal ... cannot create new msg header");
          if (NS_SUCCEEDED(rv) && newHdr) {
            srcDB->UndoDelete(newHdr);
            srcMessages.AppendElement(newHdr);
            // (we want to keep these two lists in sync)
            destMessages.AppendElement(oldHdr);
          }
        }
      }

      nsCOMPtr<nsIMsgFolderNotificationService> notifier(
          do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
      if (notifier) {
        // Remember that we're actually moving things back from the destination
        //  to the source!
        notifier->NotifyMsgsMoveCopyCompleted(true, destMessages, srcFolder,
                                              srcMessages);
      }

      nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
          do_QueryInterface(srcFolder);
      if (localFolder) {
        localFolder->MarkMsgsOnPop3Server(srcMessages,
                                          POP3_NONE /*deleteMsgs*/);
      }
    } else  // undoing a move means moving the messages back.
    {
      nsTArray<RefPtr<nsIMsgDBHdr>> dstMessages(m_dstKeyArray.Length());
      m_numHdrsCopied = 0;
      m_srcKeyArray.Clear();
      for (i = 0; i < count; i++) {
        // GetMsgHdrForKey is not a test for whether the key exists, so check.
        bool hasKey = false;
        dstDB->ContainsKey(m_dstKeyArray[i], &hasKey);
        nsCOMPtr<nsIMsgDBHdr> dstHdr;
        if (hasKey)
          dstDB->GetMsgHdrForKey(m_dstKeyArray[i], getter_AddRefs(dstHdr));
        if (dstHdr) {
          nsCString messageId;
          dstHdr->GetMessageId(getter_Copies(messageId));
          dstMessages.AppendElement(dstHdr);
          m_copiedMsgIds.AppendElement(messageId);
        } else {
          NS_WARNING("Cannot get old msg header");
        }
      }
      if (m_copiedMsgIds.Length()) {
        srcFolder->AddFolderListener(this);
        m_undoing = true;
        return srcFolder->CopyMessages(dstFolder, dstMessages, true, nullptr,
                                       nullptr, false, false);
      } else {
        // Nothing to do, probably because original messages were deleted.
        NS_WARNING("Undo did not find any messages to move");
      }
    }
    srcDB->SetSummaryValid(true);
  }

  dstDB->DeleteMessages(m_dstKeyArray, nullptr);
  dstDB->SetSummaryValid(true);

  return rv;
}

NS_IMETHODIMP
nsLocalMoveCopyMsgTxn::RedoTransaction() {
  nsresult rv;
  nsCOMPtr<nsIMsgDatabase> srcDB;
  nsCOMPtr<nsIMsgDatabase> dstDB;

  nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryReferent(m_srcFolder, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIMsgFolder> dstFolder = do_QueryReferent(m_dstFolder, &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  rv = srcFolder->GetMsgDatabase(getter_AddRefs(srcDB));
  if (NS_FAILED(rv)) return rv;
  rv = dstFolder->GetMsgDatabase(getter_AddRefs(dstDB));
  if (NS_FAILED(rv)) return rv;

  uint32_t count = m_srcKeyArray.Length();
  uint32_t i;
  nsCOMPtr<nsIMsgDBHdr> oldHdr;
  nsCOMPtr<nsIMsgDBHdr> newHdr;

  nsTArray<RefPtr<nsIMsgDBHdr>> srcMessages(m_srcKeyArray.Length());
  for (i = 0; i < count; i++) {
    rv = srcDB->GetMsgHdrForKey(m_srcKeyArray[i], getter_AddRefs(oldHdr));
    NS_ASSERTION(oldHdr, "fatal ... cannot get old msg header");

    if (NS_SUCCEEDED(rv) && oldHdr) {
      srcMessages.AppendElement(oldHdr);

      if (m_canUndelete) {
        rv = dstDB->CopyHdrFromExistingHdr(m_dstKeyArray[i], oldHdr, true,
                                           getter_AddRefs(newHdr));
        NS_ASSERTION(newHdr, "fatal ... cannot get new msg header");
        if (NS_SUCCEEDED(rv) && newHdr) {
          if (i < m_dstSizeArray.Length())
            rv = newHdr->SetMessageSize(m_dstSizeArray[i]);
          dstDB->UndoDelete(newHdr);
        }
      }
    }
  }
  dstDB->SetSummaryValid(true);

  if (m_isMove) {
    if (m_srcIsImap4) {
      // protect against a bogus undo txn without any source keys
      // see bug #179856 for details
      NS_ASSERTION(!m_srcKeyArray.IsEmpty(), "no source keys");
      if (m_srcKeyArray.IsEmpty()) return NS_ERROR_UNEXPECTED;

      bool deleteFlag = false;  // message is un-deleted- we are trying to redo
      CheckForToggleDelete(srcFolder, m_srcKeyArray[0],
                           &deleteFlag);  // there could have been a toggle
      rv = UndoImapDeleteFlag(srcFolder, m_srcKeyArray, deleteFlag);
    } else if (m_canUndelete) {
      nsCOMPtr<nsIMsgLocalMailFolder> localFolder =
          do_QueryInterface(srcFolder);
      if (localFolder) {
        localFolder->MarkMsgsOnPop3Server(srcMessages,
                                          POP3_DELETE /*deleteMsgs*/);
      }

      rv = srcDB->DeleteMessages(m_srcKeyArray, nullptr);
      srcDB->SetSummaryValid(true);
    } else {
      nsCOMPtr<nsIMsgDBHdr> srcHdr;
      m_numHdrsCopied = 0;
      m_dstKeyArray.Clear();
      for (i = 0; i < count; i++) {
        srcDB->GetMsgHdrForKey(m_srcKeyArray[i], getter_AddRefs(srcHdr));
        NS_ASSERTION(srcHdr, "fatal ... cannot get old msg header");
        if (srcHdr) {
          nsCString messageId;
          srcHdr->GetMessageId(getter_Copies(messageId));
          m_copiedMsgIds.AppendElement(messageId);
        }
      }
      dstFolder->AddFolderListener(this);
      m_undoing = false;
      return dstFolder->CopyMessages(srcFolder, srcMessages, true, nullptr,
                                     nullptr, false, false);
    }
  }

  return rv;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderAdded(nsIMsgFolder* parent,
                                                   nsIMsgFolder* child) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnMessageAdded(nsIMsgFolder* parent,
                                                    nsIMsgDBHdr* msgHdr) {
  nsresult rv;
  nsCOMPtr<nsIMsgFolder> folder =
      do_QueryReferent(m_undoing ? m_srcFolder : m_dstFolder, &rv);
  NS_ENSURE_SUCCESS(rv, rv);
  nsCString messageId;
  msgHdr->GetMessageId(getter_Copies(messageId));
  if (m_copiedMsgIds.Contains(messageId)) {
    nsMsgKey msgKey;
    msgHdr->GetMessageKey(&msgKey);
    if (m_undoing)
      m_srcKeyArray.AppendElement(msgKey);
    else
      m_dstKeyArray.AppendElement(msgKey);
    if (++m_numHdrsCopied == m_copiedMsgIds.Length()) {
      folder->RemoveFolderListener(this);
      m_copiedMsgIds.Clear();
    }
  }
  return NS_OK;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderRemoved(nsIMsgFolder* parent,
                                                     nsIMsgFolder* child) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnMessageRemoved(nsIMsgFolder* parent,
                                                      nsIMsgDBHdr* msg) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderPropertyChanged(
    nsIMsgFolder* item, const nsACString& property, const nsACString& oldValue,
    const nsACString& newValue) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderIntPropertyChanged(
    nsIMsgFolder* item, const nsACString& property, int64_t oldValue,
    int64_t newValue) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderBoolPropertyChanged(
    nsIMsgFolder* item, const nsACString& property, bool oldValue,
    bool newValue) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderUnicharPropertyChanged(
    nsIMsgFolder* item, const nsACString& property, const nsAString& oldValue,
    const nsAString& newValue) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderPropertyFlagChanged(
    nsIMsgDBHdr* item, const nsACString& property, uint32_t oldFlag,
    uint32_t newFlag) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalMoveCopyMsgTxn::OnFolderEvent(nsIMsgFolder* aItem,
                                                   const nsACString& aEvent) {
  return NS_OK;
}

NS_IMPL_ISUPPORTS(nsLocalUndoFolderListener, nsIFolderListener)

nsLocalUndoFolderListener::nsLocalUndoFolderListener(
    nsLocalMoveCopyMsgTxn* aTxn, nsIMsgFolder* aFolder) {
  mTxn = aTxn;
  mFolder = aFolder;
}

nsLocalUndoFolderListener::~nsLocalUndoFolderListener() {}

NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderAdded(nsIMsgFolder* parent,
                                                       nsIMsgFolder* child) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalUndoFolderListener::OnMessageAdded(nsIMsgFolder* parent,
                                                        nsIMsgDBHdr* msg) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderRemoved(nsIMsgFolder* parent,
                                                         nsIMsgFolder* child) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalUndoFolderListener::OnMessageRemoved(nsIMsgFolder* parent,
                                                          nsIMsgDBHdr* msg) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderPropertyChanged(
    nsIMsgFolder* item, const nsACString& property, const nsACString& oldValue,
    const nsACString& newValue) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderIntPropertyChanged(
    nsIMsgFolder* item, const nsACString& property, int64_t oldValue,
    int64_t newValue) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderBoolPropertyChanged(
    nsIMsgFolder* item, const nsACString& property, bool oldValue,
    bool newValue) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderUnicharPropertyChanged(
    nsIMsgFolder* item, const nsACString& property, const nsAString& oldValue,
    const nsAString& newValue) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderPropertyFlagChanged(
    nsIMsgDBHdr* item, const nsACString& property, uint32_t oldFlag,
    uint32_t newFlag) {
  return NS_OK;
}

NS_IMETHODIMP nsLocalUndoFolderListener::OnFolderEvent(
    nsIMsgFolder* aItem, const nsACString& aEvent) {
  if (mTxn && mFolder && aItem == mFolder) {
    if (aEvent.Equals(kFolderLoaded)) return mTxn->UndoTransactionInternal();
  }

  return NS_ERROR_FAILURE;
}
