/* -*- 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 "mozilla/mailnews/MimeHeaderParser.h"
#include "nsMsgHdr.h"
#include "nsMsgDatabase.h"
#include "nsMsgUtils.h"
#include "nsMsgMessageFlags.h"
#include "nsIMsgThread.h"
#include "mozilla/Attributes.h"
#include "nsStringEnumerator.h"
#ifdef DEBUG
#  include "nsPrintfCString.h"
#endif
using namespace mozilla::mailnews;

NS_IMPL_ISUPPORTS(nsMsgHdr, nsIMsgDBHdr)

#define FLAGS_INITED 0x1
#define CACHED_VALUES_INITED 0x2
#define REFERENCES_INITED 0x4
#define THREAD_PARENT_INITED 0x8

nsMsgHdr::nsMsgHdr(nsMsgDatabase* db, nsIMdbRow* dbRow) {
  m_mdb = db;
  Init();
  m_mdbRow = dbRow;
  if (m_mdb) {
    NS_ADDREF(m_mdb);  // Released in DTOR.
    mdbOid outOid;
    if (dbRow && NS_SUCCEEDED(dbRow->GetOid(m_mdb->GetEnv(), &outOid))) {
      m_messageKey = outOid.mOid_Id;
      m_mdb->AddHdrToUseCache((nsIMsgDBHdr*)this, m_messageKey);
    }
  }
}

void nsMsgHdr::Init() {
  m_initedValues = 0;
  m_messageKey = nsMsgKey_None;
  m_messageSize = 0;
  m_date = 0;
  m_flags = 0;
  m_mdbRow = NULL;
  m_threadId = nsMsgKey_None;
  m_threadParent = nsMsgKey_None;
}

nsresult nsMsgHdr::InitCachedValues() {
  nsresult err = NS_OK;

  if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;

  if (!(m_initedValues & CACHED_VALUES_INITED)) {
    uint32_t uint32Value;
    mdbOid outOid;
    if (NS_SUCCEEDED(m_mdbRow->GetOid(m_mdb->GetEnv(), &outOid)))
      m_messageKey = outOid.mOid_Id;

    err = GetUInt32Column(m_mdb->m_messageSizeColumnToken, &m_messageSize);

    err = GetUInt32Column(m_mdb->m_dateColumnToken, &uint32Value);
    Seconds2PRTime(uint32Value, &m_date);

    err = GetUInt32Column(m_mdb->m_messageThreadIdColumnToken, &m_threadId);

    if (NS_SUCCEEDED(err)) m_initedValues |= CACHED_VALUES_INITED;
  }
  return err;
}

nsresult nsMsgHdr::InitFlags() {
  nsresult err = NS_OK;

  if (!m_mdb) return NS_ERROR_NULL_POINTER;

  if (!(m_initedValues & FLAGS_INITED)) {
    err = GetUInt32Column(m_mdb->m_flagsColumnToken, &m_flags);
    m_flags &= ~nsMsgMessageFlags::New;  // don't get new flag from MDB

    if (NS_SUCCEEDED(err)) m_initedValues |= FLAGS_INITED;
  }

  return err;
}

nsMsgHdr::~nsMsgHdr() {
  if (m_mdbRow) {
    if (m_mdb) {
      NS_RELEASE(m_mdbRow);
      m_mdb->RemoveHdrFromUseCache((nsIMsgDBHdr*)this, m_messageKey);
    }
  }
  NS_IF_RELEASE(m_mdb);
}

NS_IMETHODIMP nsMsgHdr::GetMessageKey(nsMsgKey* result) {
  if (m_messageKey == nsMsgKey_None && m_mdbRow != NULL) {
    mdbOid outOid;
    if (NS_SUCCEEDED(m_mdbRow->GetOid(m_mdb->GetEnv(), &outOid)))
      m_messageKey = outOid.mOid_Id;
  }
  *result = m_messageKey;
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetThreadId(nsMsgKey* result) {
  if (!(m_initedValues & CACHED_VALUES_INITED)) InitCachedValues();

  if (result) {
    *result = m_threadId;
    return NS_OK;
  }
  return NS_ERROR_NULL_POINTER;
}

NS_IMETHODIMP nsMsgHdr::SetThreadId(nsMsgKey inKey) {
  m_threadId = inKey;
  return SetUInt32Column(m_threadId, m_mdb->m_messageThreadIdColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetMessageKey(nsMsgKey value) {
  m_messageKey = value;
  return NS_OK;
}

nsresult nsMsgHdr::GetRawFlags(uint32_t* result) {
  if (!(m_initedValues & FLAGS_INITED)) InitFlags();
  *result = m_flags;
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetFlags(uint32_t* result) {
  if (!(m_initedValues & FLAGS_INITED)) InitFlags();
  if (m_mdb)
    *result = m_mdb->GetStatusFlags(this, m_flags);
  else
    *result = m_flags;
#ifdef DEBUG_bienvenu
  NS_ASSERTION(!(*result & (nsMsgMessageFlags::Elided)),
               "shouldn't be set in db");
#endif
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::SetFlags(uint32_t flags) {
#ifdef DEBUG_bienvenu
  NS_ASSERTION(!(flags & (nsMsgMessageFlags::Elided)),
               "shouldn't set this flag on db");
#endif
  m_initedValues |= FLAGS_INITED;
  m_flags = flags;
  // don't write out nsMsgMessageFlags::New to MDB.
  return SetUInt32Column(m_flags & ~nsMsgMessageFlags::New,
                         m_mdb->m_flagsColumnToken);
}

NS_IMETHODIMP nsMsgHdr::OrFlags(uint32_t flags, uint32_t* result) {
  if (!(m_initedValues & FLAGS_INITED)) InitFlags();
  if ((m_flags & flags) != flags) SetFlags(m_flags | flags);
  *result = m_flags;
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::AndFlags(uint32_t flags, uint32_t* result) {
  if (!(m_initedValues & FLAGS_INITED)) InitFlags();
  if ((m_flags & flags) != m_flags) SetFlags(m_flags & flags);
  *result = m_flags;
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::MarkHasAttachments(bool bHasAttachments) {
  nsresult rv = NS_OK;

  if (m_mdb) {
    nsMsgKey key;
    rv = GetMessageKey(&key);
    if (NS_SUCCEEDED(rv))
      rv = m_mdb->MarkHasAttachments(key, bHasAttachments, nullptr);
  }
  return rv;
}

NS_IMETHODIMP nsMsgHdr::MarkRead(bool bRead) {
  nsresult rv = NS_OK;

  if (m_mdb) {
    nsMsgKey key;
    rv = GetMessageKey(&key);
    if (NS_SUCCEEDED(rv)) rv = m_mdb->MarkRead(key, bRead, nullptr);
  }
  return rv;
}

NS_IMETHODIMP nsMsgHdr::MarkFlagged(bool bFlagged) {
  nsresult rv = NS_OK;

  if (m_mdb) {
    nsMsgKey key;
    rv = GetMessageKey(&key);
    if (NS_SUCCEEDED(rv)) rv = m_mdb->MarkMarked(key, bFlagged, nullptr);
  }
  return rv;
}

NS_IMETHODIMP nsMsgHdr::SetStringProperty(const char* propertyName,
                                          const nsACString& propertyValue) {
  NS_ENSURE_ARG_POINTER(propertyName);
  if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;
  return m_mdb->SetProperty(m_mdbRow, propertyName,
                            PromiseFlatCString(propertyValue).get());
}

NS_IMETHODIMP nsMsgHdr::GetStringProperty(const char* propertyName,
                                          nsACString& aPropertyValue) {
  NS_ENSURE_ARG_POINTER(propertyName);
  if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;
  return m_mdb->GetProperty(m_mdbRow, propertyName,
                            getter_Copies(aPropertyValue));
}

NS_IMETHODIMP nsMsgHdr::GetUint32Property(const char* propertyName,
                                          uint32_t* pResult) {
  NS_ENSURE_ARG_POINTER(propertyName);
  if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;
  return m_mdb->GetUint32Property(m_mdbRow, propertyName, pResult);
}

NS_IMETHODIMP nsMsgHdr::SetUint32Property(const char* propertyName,
                                          uint32_t value) {
  NS_ENSURE_ARG_POINTER(propertyName);
  if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER;
  return m_mdb->SetUint32Property(m_mdbRow, propertyName, value);
}

NS_IMETHODIMP nsMsgHdr::GetNumReferences(uint16_t* result) {
  if (!(m_initedValues & REFERENCES_INITED)) {
    const char* references;
    if (NS_SUCCEEDED(m_mdb->RowCellColumnToConstCharPtr(
            GetMDBRow(), m_mdb->m_referencesColumnToken, &references)))
      ParseReferences(references);
    m_initedValues |= REFERENCES_INITED;
  }

  if (result) *result = m_references.Length();
  // there is no real failure here; if there are no references, there are no
  //  references.
  return NS_OK;
}

nsresult nsMsgHdr::ParseReferences(const char* references) {
  const char* startNextRef = references;
  nsAutoCString resultReference;
  nsCString messageId;
  GetMessageId(getter_Copies(messageId));

  while (startNextRef && *startNextRef) {
    startNextRef = GetNextReference(startNextRef, resultReference,
                                    startNextRef == references);
    // Don't add self-references.
    if (!resultReference.IsEmpty() && !resultReference.Equals(messageId))
      m_references.AppendElement(resultReference);
  }
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetStringReference(int32_t refNum,
                                           nsACString& resultReference) {
  nsresult err = NS_OK;

  if (!(m_initedValues & REFERENCES_INITED))
    GetNumReferences(nullptr);  // it can handle the null

  if ((uint32_t)refNum < m_references.Length())
    resultReference = m_references.ElementAt(refNum);
  else
    err = NS_ERROR_ILLEGAL_VALUE;
  return err;
}

NS_IMETHODIMP nsMsgHdr::GetDate(PRTime* result) {
  if (!(m_initedValues & CACHED_VALUES_INITED)) InitCachedValues();

  *result = m_date;
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetDateInSeconds(uint32_t* aResult) {
  return GetUInt32Column(m_mdb->m_dateColumnToken, aResult);
}

NS_IMETHODIMP nsMsgHdr::SetMessageId(const char* messageId) {
  if (messageId && *messageId == '<') {
    nsAutoCString tempMessageID(messageId + 1);
    if (tempMessageID.CharAt(tempMessageID.Length() - 1) == '>')
      tempMessageID.SetLength(tempMessageID.Length() - 1);
    return SetStringColumn(tempMessageID.get(), m_mdb->m_messageIdColumnToken);
  }
  return SetStringColumn(messageId, m_mdb->m_messageIdColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetSubject(const nsACString& subject) {
  return SetStringColumn(PromiseFlatCString(subject).get(),
                         m_mdb->m_subjectColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetAuthor(const char* author) {
  return SetStringColumn(author, m_mdb->m_senderColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetReferences(const nsACString& references) {
  m_references.Clear();
  ParseReferences(PromiseFlatCString(references).get());

  m_initedValues |= REFERENCES_INITED;

  return SetStringColumn(PromiseFlatCString(references).get(),
                         m_mdb->m_referencesColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetRecipients(const char* recipients) {
  // need to put in rfc822 address parsing code here (or make caller do it...)
  return SetStringColumn(recipients, m_mdb->m_recipientsColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetCcList(const char* ccList) {
  return SetStringColumn(ccList, m_mdb->m_ccListColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetBccList(const char* bccList) {
  return SetStringColumn(bccList, m_mdb->m_bccListColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetMessageSize(uint32_t messageSize) {
  SetUInt32Column(messageSize, m_mdb->m_messageSizeColumnToken);
  m_messageSize = messageSize;
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetOfflineMessageSize(uint32_t* result) {
  uint32_t size;
  nsresult res = GetUInt32Column(m_mdb->m_offlineMessageSizeColumnToken, &size);

  *result = size;
  return res;
}

NS_IMETHODIMP nsMsgHdr::SetOfflineMessageSize(uint32_t messageSize) {
  return SetUInt32Column(messageSize, m_mdb->m_offlineMessageSizeColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetLineCount(uint32_t lineCount) {
  SetUInt32Column(lineCount, m_mdb->m_numLinesColumnToken);
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::SetDate(PRTime date) {
  m_date = date;
  uint32_t seconds;
  PRTime2Seconds(date, &seconds);
  return SetUInt32Column((uint32_t)seconds, m_mdb->m_dateColumnToken);
}

NS_IMETHODIMP nsMsgHdr::SetPriority(nsMsgPriorityValue priority) {
  SetUInt32Column((uint32_t)priority, m_mdb->m_priorityColumnToken);
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetPriority(nsMsgPriorityValue* result) {
  if (!result) return NS_ERROR_NULL_POINTER;

  uint32_t priority = 0;
  nsresult rv = GetUInt32Column(m_mdb->m_priorityColumnToken, &priority);
  if (NS_FAILED(rv)) return rv;

  *result = (nsMsgPriorityValue)priority;
  return NS_OK;
}

// I'd like to not store the account key, if the msg is in
// the same account as it was received in, to save disk space and memory.
// This might be problematic when a message gets moved...
// And I'm not sure if we should short circuit it here,
// or at a higher level where it might be more efficient.
NS_IMETHODIMP nsMsgHdr::SetAccountKey(const char* aAccountKey) {
  return SetStringProperty("account", nsDependentCString(aAccountKey));
}

NS_IMETHODIMP nsMsgHdr::GetAccountKey(char** aResult) {
  NS_ENSURE_ARG_POINTER(aResult);

  nsCString key;
  nsresult rv = GetStringProperty("account", key);
  NS_ENSURE_SUCCESS(rv, rv);
  *aResult = ToNewCString(key);
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetMessageOffset(uint64_t* result) {
  NS_ENSURE_ARG(result);

  (void)GetUInt64Column(m_mdb->m_offlineMsgOffsetColumnToken, result,
                        (unsigned)-1);
  if (*result == (unsigned)-1) {
    // It's unset. Unfortunately there's not much we can do here. There's
    // a lot of code which relies on being able to read .messageOffset,
    // even if it doesn't require it to return anything sensible.
    // (For example - in js unit tests - Assert.equals() stringifies the
    // attributes of it's expected/actual values to produce an error
    // message even if the assert passes).
#ifdef DEBUG
    nsCString tok;
    GetStringProperty("storeToken", tok);
    nsPrintfCString err("Missing .messageOffset (key=%u, storeToken='%s')",
                        m_messageKey, tok.get());
    NS_WARNING(err.get());
#endif
    // Return something obviously broken.
    *result = 12345678;
  }
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::SetMessageOffset(uint64_t offset) {
  SetUInt64Column(offset, m_mdb->m_offlineMsgOffsetColumnToken);
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetMessageSize(uint32_t* result) {
  uint32_t size;
  nsresult res = GetUInt32Column(m_mdb->m_messageSizeColumnToken, &size);

  *result = size;
  return res;
}

NS_IMETHODIMP nsMsgHdr::GetLineCount(uint32_t* result) {
  uint32_t linecount;
  nsresult res = GetUInt32Column(m_mdb->m_numLinesColumnToken, &linecount);
  *result = linecount;
  return res;
}

NS_IMETHODIMP nsMsgHdr::GetAuthor(char** resultAuthor) {
  return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_senderColumnToken,
                                       resultAuthor);
}

NS_IMETHODIMP nsMsgHdr::GetSubject(nsACString& resultSubject) {
  return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_subjectColumnToken,
                                       getter_Copies(resultSubject));
}

NS_IMETHODIMP nsMsgHdr::GetRecipients(char** resultRecipients) {
  return m_mdb->RowCellColumnToCharPtr(
      GetMDBRow(), m_mdb->m_recipientsColumnToken, resultRecipients);
}

NS_IMETHODIMP nsMsgHdr::GetCcList(char** resultCCList) {
  return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_ccListColumnToken,
                                       resultCCList);
}

NS_IMETHODIMP nsMsgHdr::GetBccList(char** resultBCCList) {
  return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_bccListColumnToken,
                                       resultBCCList);
}

NS_IMETHODIMP nsMsgHdr::GetMessageId(char** resultMessageId) {
  return m_mdb->RowCellColumnToCharPtr(
      GetMDBRow(), m_mdb->m_messageIdColumnToken, resultMessageId);
}

NS_IMETHODIMP nsMsgHdr::GetMime2DecodedAuthor(nsAString& resultAuthor) {
  return m_mdb->RowCellColumnToMime2DecodedString(
      GetMDBRow(), m_mdb->m_senderColumnToken, resultAuthor);
}

NS_IMETHODIMP nsMsgHdr::GetMime2DecodedSubject(nsAString& resultSubject) {
  return m_mdb->RowCellColumnToMime2DecodedString(
      GetMDBRow(), m_mdb->m_subjectColumnToken, resultSubject);
}

NS_IMETHODIMP nsMsgHdr::GetMime2DecodedRecipients(nsAString& resultRecipients) {
  return m_mdb->RowCellColumnToMime2DecodedString(
      GetMDBRow(), m_mdb->m_recipientsColumnToken, resultRecipients);
}

NS_IMETHODIMP nsMsgHdr::GetAuthorCollationKey(nsTArray<uint8_t>& resultAuthor) {
  return m_mdb->RowCellColumnToAddressCollationKey(
      GetMDBRow(), m_mdb->m_senderColumnToken, resultAuthor);
}

NS_IMETHODIMP nsMsgHdr::GetSubjectCollationKey(
    nsTArray<uint8_t>& resultSubject) {
  return m_mdb->RowCellColumnToCollationKey(
      GetMDBRow(), m_mdb->m_subjectColumnToken, resultSubject);
}

NS_IMETHODIMP nsMsgHdr::GetRecipientsCollationKey(
    nsTArray<uint8_t>& resultRecipients) {
  return m_mdb->RowCellColumnToCollationKey(
      GetMDBRow(), m_mdb->m_recipientsColumnToken, resultRecipients);
}

NS_IMETHODIMP nsMsgHdr::GetCharset(char** aCharset) {
  return m_mdb->RowCellColumnToCharPtr(
      GetMDBRow(), m_mdb->m_messageCharSetColumnToken, aCharset);
}

NS_IMETHODIMP nsMsgHdr::SetCharset(const char* aCharset) {
  return SetStringColumn(aCharset, m_mdb->m_messageCharSetColumnToken);
}

NS_IMETHODIMP nsMsgHdr::GetEffectiveCharset(nsACString& resultCharset) {
  return m_mdb->GetEffectiveCharset(m_mdbRow, resultCharset);
}

NS_IMETHODIMP nsMsgHdr::SetThreadParent(nsMsgKey inKey) {
  m_threadParent = inKey;
  if (inKey == m_messageKey) NS_ASSERTION(false, "can't be your own parent");
  SetUInt32Column(m_threadParent, m_mdb->m_threadParentColumnToken);
  m_initedValues |= THREAD_PARENT_INITED;
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetThreadParent(nsMsgKey* result) {
  nsresult res;
  if (!(m_initedValues & THREAD_PARENT_INITED)) {
    res = GetUInt32Column(m_mdb->m_threadParentColumnToken, &m_threadParent,
                          nsMsgKey_None);
    if (NS_SUCCEEDED(res)) m_initedValues |= THREAD_PARENT_INITED;
  }
  *result = m_threadParent;
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetFolder(nsIMsgFolder** result) {
  NS_ENSURE_ARG(result);

  if (m_mdb && m_mdb->m_folder) {
    NS_ADDREF(*result = m_mdb->m_folder);
  } else
    *result = nullptr;
  return NS_OK;
}

nsresult nsMsgHdr::SetStringColumn(const char* str, mdb_token token) {
  NS_ENSURE_ARG_POINTER(str);
  return m_mdb->CharPtrToRowCellColumn(m_mdbRow, token, str);
}

nsresult nsMsgHdr::SetUInt32Column(uint32_t value, mdb_token token) {
  return m_mdb->UInt32ToRowCellColumn(m_mdbRow, token, value);
}

nsresult nsMsgHdr::GetUInt32Column(mdb_token token, uint32_t* pvalue,
                                   uint32_t defaultValue) {
  return m_mdb->RowCellColumnToUInt32(GetMDBRow(), token, pvalue, defaultValue);
}

nsresult nsMsgHdr::SetUInt64Column(uint64_t value, mdb_token token) {
  return m_mdb->UInt64ToRowCellColumn(m_mdbRow, token, value);
}

nsresult nsMsgHdr::GetUInt64Column(mdb_token token, uint64_t* pvalue,
                                   uint64_t defaultValue) {
  return m_mdb->RowCellColumnToUInt64(GetMDBRow(), token, pvalue, defaultValue);
}

/**
 * Roughly speaking, get the next message-id (starts with a '<' ends with a
 *  '>').  Except, we also try to handle the case where your reference is of
 *  a prehistoric vintage that just stuck any old random junk in there.  Our
 *  old logic would (unintentionally?) just trim the whitespace off the front
 *  and hand you everything after that.  We change things at all because that
 *  same behaviour does not make sense if we have already seen a proper message
 *  id.  We keep the old behaviour at all because it would seem to have
 *  benefits.  (See jwz's non-zero stats: http://www.jwz.org/doc/threading.html)
 * So, to re-state, if there is a valid message-id in there at all, we only
 *  return valid message-id's (sans bracketing '<' and '>').  If there isn't,
 *  our result (via "references") is a left-trimmed copy of the string.  If
 *  there is nothing in there, our result is an empty string.)  We do require
 *  that you pass allowNonDelimitedReferences what it demands, though.
 * For example: "<valid@stuff> this stuff is invalid" would net you
 *  "valid@stuff" and "this stuff is invalid" as results.  We now only would
 *  provide "valid-stuff" and an empty string (which you should ignore) as
 *  results.  However "this stuff is invalid" would return itself, allowing
 *  anything relying on that behaviour to keep working.
 *
 * Note: We accept anything inside the '<' and '>'; technically, we should want
 *  at least a '@' in there (per rfc 2822).  But since we're going out of our
 *  way to support weird things...
 *
 * @param startNextRef The position to start at; this should either be the start
 *     of your references string or our return value from a previous call.
 * @param reference You pass a nsCString by reference, we put the reference we
 *     find in it, if we find one.  It may be empty!  Beware!
 * @param allowNonDelimitedReferences Should we support the
 *     pre-reasonable-standards form of In-Reply-To where it could be any
 *     arbitrary string and our behaviour was just to take off leading
 *     whitespace.  It only makes sense to pass true for your first call to this
 *     function, as if you are around to make a second call, it means we found
 *     a properly formatted message-id and so we should only look for more
 *     properly formatted message-ids.
 *     NOTE: this option will also strip off a single leading '<' if there is
 *     one. Some examples:
 *        "   foo" => "foo"
 *        "  <bar" => "bar"
 *        "<<<foo" => "<<foo"
 *        "<foo@bar>" => "foo@bar"  (completed message-id)
 * @returns The next starting position of this routine, which may be pointing at
 *     a nul '\0' character to indicate termination.
 */
const char* nsMsgHdr::GetNextReference(const char* startNextRef,
                                       nsCString& reference,
                                       bool acceptNonDelimitedReferences) {
  const char* ptr = startNextRef;
  const char* whitespaceEndedAt = nullptr;
  const char* firstMessageIdChar = nullptr;

  // make the reference result string empty by default; we will set it to
  //  something valid if the time comes.
  reference.Truncate();

  // walk until we find a '<', but keep track of the first point we found that
  //  was not whitespace (as defined by previous versions of this code.)
  for (bool foundLessThan = false; !foundLessThan; ptr++) {
    switch (*ptr) {
      case '\0':
        // if we are at the end of the string, we found some non-whitespace, and
        //  the caller requested that we accept non-delimited whitespace,
        //  give them that as their reference.  (otherwise, leave it empty)
        if (acceptNonDelimitedReferences && whitespaceEndedAt)
          reference = whitespaceEndedAt;
        return ptr;
      case ' ':
      case '\r':
      case '\n':
      case '\t':
        // do nothing, make default case mean you didn't get whitespace
        break;
      case '<':
        firstMessageIdChar = ptr + 1;  // skip over the '<'
        foundLessThan = true;          // (flag to stop)
        // Ensure whitespaceEndedAt skips the leading '<' and is set to
        //  a non-NULL value, just in case the message-id is not valid (no '>')
        //  and the old-school support is desired.
        if (!whitespaceEndedAt) whitespaceEndedAt = ptr + 1;
        break;
      default:
        if (!whitespaceEndedAt) whitespaceEndedAt = ptr;
        break;
    }
  }

  // keep going until we hit a '>' or hit the end of the string
  for (; *ptr; ptr++) {
    if (*ptr == '>') {
      // it's valid, update reference, making sure to stop before the '>'
      reference.Assign(firstMessageIdChar, ptr - firstMessageIdChar);
      // and return a start point just after the '>'
      return ++ptr;
    }
  }

  // we did not have a fully-formed, valid message-id, so consider falling back
  if (acceptNonDelimitedReferences && whitespaceEndedAt)
    reference = whitespaceEndedAt;
  return ptr;
}

bool nsMsgHdr::IsParentOf(nsIMsgDBHdr* possibleChild) {
  uint16_t referenceToCheck = 0;
  possibleChild->GetNumReferences(&referenceToCheck);
  nsAutoCString reference;

  nsCString messageId;
  GetMessageId(getter_Copies(messageId));

  while (referenceToCheck > 0) {
    possibleChild->GetStringReference(referenceToCheck - 1, reference);

    if (reference.Equals(messageId)) return true;
    // if reference didn't match, check if this ref is for a non-existent
    // header. If it is, continue looking at ancestors.
    nsCOMPtr<nsIMsgDBHdr> refHdr;
    if (!m_mdb) break;
    (void)m_mdb->GetMsgHdrForMessageID(reference.get(), getter_AddRefs(refHdr));
    if (refHdr) break;
    referenceToCheck--;
  }
  return false;
}

bool nsMsgHdr::IsAncestorOf(nsIMsgDBHdr* possibleChild) {
  const char* references;
  nsMsgHdr* curHdr =
      static_cast<nsMsgHdr*>(possibleChild);  // closed system, cast ok
  m_mdb->RowCellColumnToConstCharPtr(
      curHdr->GetMDBRow(), m_mdb->m_referencesColumnToken, &references);
  if (!references) return false;

  nsCString messageId;
  // should put < > around message id to make strstr strictly match
  GetMessageId(getter_Copies(messageId));
  return (strstr(references, messageId.get()) != nullptr);
}

NS_IMETHODIMP nsMsgHdr::GetIsRead(bool* isRead) {
  NS_ENSURE_ARG_POINTER(isRead);
  if (!(m_initedValues & FLAGS_INITED)) InitFlags();
  *isRead = !!(m_flags & nsMsgMessageFlags::Read);
  return NS_OK;
}

NS_IMETHODIMP nsMsgHdr::GetIsFlagged(bool* isFlagged) {
  NS_ENSURE_ARG_POINTER(isFlagged);
  if (!(m_initedValues & FLAGS_INITED)) InitFlags();
  *isFlagged = !!(m_flags & nsMsgMessageFlags::Marked);
  return NS_OK;
}

void nsMsgHdr::ReparentInThread(nsIMsgThread* thread) {
  NS_WARNING("Borked message header, attempting to fix!");
  uint32_t numChildren;
  thread->GetNumChildren(&numChildren);
  // bail out early for the singleton thread case.
  if (numChildren == 1) {
    SetThreadParent(nsMsgKey_None);
    return;
  } else {
    nsCOMPtr<nsIMsgDBHdr> curHdr;
    // loop through thread, looking for our proper parent.
    for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
      thread->GetChildHdrAt(childIndex, getter_AddRefs(curHdr));
      // closed system, cast ok
      nsMsgHdr* curMsgHdr = static_cast<nsMsgHdr*>(curHdr.get());
      if (curHdr && curMsgHdr->IsParentOf(this)) {
        nsMsgKey curHdrKey;
        curHdr->GetMessageKey(&curHdrKey);
        SetThreadParent(curHdrKey);
        return;
      }
    }
    // we didn't find it. So either the root header is our parent,
    // or we're the root.
    nsCOMPtr<nsIMsgDBHdr> rootHdr;
    thread->GetRootHdr(getter_AddRefs(rootHdr));
    NS_ASSERTION(rootHdr, "thread has no root hdr - shouldn't happen");
    if (rootHdr) {
      nsMsgKey rootKey;
      rootHdr->GetMessageKey(&rootKey);
      // if we're the root, our thread parent is -1.
      SetThreadParent(rootKey == m_messageKey ? nsMsgKey_None : rootKey);
    }
  }
}

bool nsMsgHdr::IsAncestorKilled(uint32_t ancestorsToCheck) {
  if (!(m_initedValues & FLAGS_INITED)) InitFlags();
  bool isKilled = m_flags & nsMsgMessageFlags::Ignored;

  if (!isKilled) {
    nsMsgKey threadParent;
    GetThreadParent(&threadParent);

    if (threadParent == m_messageKey) {
      // isKilled is false by virtue of the enclosing if statement
      NS_ERROR("Thread is parent of itself, please fix!");
      nsCOMPtr<nsIMsgThread> thread;
      (void)m_mdb->GetThreadContainingMsgHdr(this, getter_AddRefs(thread));
      if (!thread) return false;
      ReparentInThread(thread);
      // Something's wrong, but the problem happened some time ago, so erroring
      // out now is probably not a good idea. Ergo, we'll pretend to be OK, show
      // the user the thread (err on the side of caution), and let the assertion
      // alert debuggers to a problem.
      return false;
    }
    if (threadParent != nsMsgKey_None) {
      nsCOMPtr<nsIMsgDBHdr> parentHdr;
      (void)m_mdb->GetMsgHdrForKey(threadParent, getter_AddRefs(parentHdr));

      if (parentHdr) {
        // More proofing against crashers. This crasher was derived from the
        // fact that something got borked, leaving is in hand with a circular
        // reference to borked headers inducing these loops. The defining
        // characteristic of these headers is that they don't actually seat
        // themselves in the thread properly.
        nsCOMPtr<nsIMsgThread> thread;
        (void)m_mdb->GetThreadContainingMsgHdr(this, getter_AddRefs(thread));
        if (thread) {
          nsCOMPtr<nsIMsgDBHdr> claimant;
          (void)thread->GetChild(threadParent, getter_AddRefs(claimant));
          if (!claimant) {
            // attempt to reparent, and say the thread isn't killed,
            // erring on the side of safety.
            ReparentInThread(thread);
            return false;
          }
        }

        if (!ancestorsToCheck) {
          // We think we have a parent, but we have no more ancestors to check
          NS_ASSERTION(false, "cycle in parent relationship, please fix!");
          return false;
        }
        // closed system, cast ok
        nsMsgHdr* parent = static_cast<nsMsgHdr*>(parentHdr.get());
        return parent->IsAncestorKilled(ancestorsToCheck - 1);
      }
    }
  }
  return isKilled;
}

NS_IMETHODIMP nsMsgHdr::GetIsKilled(bool* isKilled) {
  NS_ENSURE_ARG_POINTER(isKilled);
  *isKilled = false;
  nsCOMPtr<nsIMsgThread> thread;
  (void)m_mdb->GetThreadContainingMsgHdr(this, getter_AddRefs(thread));
  // if we can't find the thread, let's at least check one level; maybe
  // the header hasn't been added to a thread yet.
  uint32_t numChildren = 1;
  if (thread) thread->GetNumChildren(&numChildren);
  if (!numChildren) return NS_ERROR_FAILURE;
  // We can't have as many ancestors as there are messages in the thread,
  // so tell IsAncestorKilled to only check numChildren - 1 ancestors.
  *isKilled = IsAncestorKilled(numChildren - 1);
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////

#include "nsIStringEnumerator.h"
#define NULL_MORK_COLUMN 0
class nsMsgPropertyEnumerator : public nsStringEnumeratorBase {
 public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIUTF8STRINGENUMERATOR

  using nsStringEnumeratorBase::GetNext;

  explicit nsMsgPropertyEnumerator(nsMsgHdr* aHdr);
  void PrefetchNext();

 protected:
  virtual ~nsMsgPropertyEnumerator();
  nsCOMPtr<nsIMdbRowCellCursor> mRowCellCursor;
  nsCOMPtr<nsIMdbEnv> m_mdbEnv;
  nsCOMPtr<nsIMdbStore> m_mdbStore;
  // Hold a reference to the hdr so it will hold an xpcom reference to the
  // underlying mdb row. The row cell cursor will crash if the underlying
  // row goes away.
  RefPtr<nsMsgHdr> m_hdr;
  bool mNextPrefetched;
  mdb_column mNextColumn;
};

nsMsgPropertyEnumerator::nsMsgPropertyEnumerator(nsMsgHdr* aHdr)
    : mNextPrefetched(false), mNextColumn(NULL_MORK_COLUMN) {
  RefPtr<nsMsgDatabase> mdb;
  nsCOMPtr<nsIMdbRow> mdbRow;

  if (aHdr && (mdbRow = aHdr->GetMDBRow()) && (m_hdr = aHdr) &&
      (mdb = aHdr->GetMdb()) && (m_mdbEnv = mdb->m_mdbEnv) &&
      (m_mdbStore = mdb->m_mdbStore)) {
    mdbRow->GetRowCellCursor(m_mdbEnv, -1, getter_AddRefs(mRowCellCursor));
  }
}

nsMsgPropertyEnumerator::~nsMsgPropertyEnumerator() {
  // Need to clear this before the nsMsgHdr and its corresponding
  // nsIMdbRow potentially go away.
  mRowCellCursor = nullptr;
}

NS_IMPL_ISUPPORTS(nsMsgPropertyEnumerator, nsIUTF8StringEnumerator,
                  nsIStringEnumerator)

NS_IMETHODIMP nsMsgPropertyEnumerator::GetNext(nsACString& aItem) {
  PrefetchNext();
  if (mNextColumn == NULL_MORK_COLUMN)
    return NS_ERROR_FAILURE;  // call HasMore first
  if (!m_mdbStore || !m_mdbEnv) return NS_ERROR_NOT_INITIALIZED;
  mNextPrefetched = false;
  char columnName[100];
  struct mdbYarn colYarn = {columnName, 0, sizeof(columnName), 0, 0, nullptr};
  // Get the column of the cell
  nsresult rv = m_mdbStore->TokenToString(m_mdbEnv, mNextColumn, &colYarn);
  NS_ENSURE_SUCCESS(rv, rv);

  aItem.Assign(static_cast<char*>(colYarn.mYarn_Buf), colYarn.mYarn_Fill);
  return NS_OK;
}

NS_IMETHODIMP nsMsgPropertyEnumerator::HasMore(bool* aResult) {
  NS_ENSURE_ARG_POINTER(aResult);

  PrefetchNext();
  *aResult = (mNextColumn != NULL_MORK_COLUMN);
  return NS_OK;
}

void nsMsgPropertyEnumerator::PrefetchNext(void) {
  if (!mNextPrefetched && m_mdbEnv && mRowCellCursor) {
    mNextPrefetched = true;
    nsCOMPtr<nsIMdbCell> cell;
    mRowCellCursor->NextCell(m_mdbEnv, getter_AddRefs(cell), &mNextColumn,
                             nullptr);
    if (mNextColumn == NULL_MORK_COLUMN) {
      // free up references
      m_mdbStore = nullptr;
      m_mdbEnv = nullptr;
      mRowCellCursor = nullptr;
    }
  }
}

////////////////////////////////////////////////////////////////////////////////

NS_IMETHODIMP nsMsgHdr::GetProperties(nsTArray<nsCString>& headers) {
  nsCOMPtr<nsIUTF8StringEnumerator> propertyEnumerator =
      new nsMsgPropertyEnumerator(this);
  bool hasMore;
  while (NS_SUCCEEDED(propertyEnumerator->HasMore(&hasMore)) && hasMore) {
    nsAutoCString property;
    propertyEnumerator->GetNext(property);
    headers.AppendElement(property);
  }
  return NS_OK;
}
