/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sts=2 sw=2 tw=80: */
/* 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 "EditorSpellCheck.h"

#include "EditorBase.h"            // for EditorBase
#include "HTMLEditor.h"            // for HTMLEditor
#include "TextServicesDocument.h"  // for TextServicesDocument

#include "mozilla/Attributes.h"   // for final
#include "mozilla/dom/Element.h"  // for Element
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/StaticRange.h"
#include "mozilla/intl/Locale.h"         // for mozilla::intl::Locale
#include "mozilla/intl/LocaleService.h"  // for retrieving app locale
#include "mozilla/intl/OSPreferences.h"  // for mozilla::intl::OSPreferences
#include "mozilla/Logging.h"             // for mozilla::LazyLogModule
#include "mozilla/mozalloc.h"            // for operator delete, etc
#include "mozilla/mozSpellChecker.h"     // for mozSpellChecker
#include "mozilla/Preferences.h"         // for Preferences

#include "nsAString.h"                // for nsAString::IsEmpty, etc
#include "nsComponentManagerUtils.h"  // for do_CreateInstance
#include "nsDebug.h"                  // for NS_ENSURE_TRUE, etc
#include "nsDependentSubstring.h"     // for Substring
#include "nsError.h"                  // for NS_ERROR_NOT_INITIALIZED, etc
#include "nsIContent.h"               // for nsIContent
#include "nsIContentPrefService2.h"   // for nsIContentPrefService2, etc
#include "mozilla/dom/Document.h"     // for Document
#include "nsIEditor.h"                // for nsIEditor
#include "nsILoadContext.h"
#include "nsISupports.h"       // for nsISupports
#include "nsISupportsUtils.h"  // for NS_ADDREF
#include "nsIURI.h"            // for nsIURI
#include "nsThreadUtils.h"     // for GetMainThreadSerialEventTarget
#include "nsVariant.h"         // for nsIWritableVariant, etc
#include "nsLiteralString.h"   // for NS_LITERAL_STRING, etc
#include "nsRange.h"
#include "nsReadableUtils.h"        // for ToNewUnicode, EmptyString, etc
#include "nsServiceManagerUtils.h"  // for do_GetService
#include "nsString.h"               // for nsAutoString, nsString, etc
#include "nsStringFwd.h"            // for nsAFlatString
#include "nsStyleUtil.h"            // for nsStyleUtil
#include "nsXULAppAPI.h"            // for XRE_GetProcessType

namespace mozilla {

using namespace dom;
using intl::LocaleService;
using intl::OSPreferences;

static mozilla::LazyLogModule sEditorSpellChecker("EditorSpellChecker");

class UpdateDictionaryHolder {
 private:
  EditorSpellCheck* mSpellCheck;

 public:
  explicit UpdateDictionaryHolder(EditorSpellCheck* esc) : mSpellCheck(esc) {
    if (mSpellCheck) {
      mSpellCheck->BeginUpdateDictionary();
    }
  }

  ~UpdateDictionaryHolder() {
    if (mSpellCheck) {
      mSpellCheck->EndUpdateDictionary();
    }
  }
};

#define CPS_PREF_NAME u"spellcheck.lang"_ns

/**
 * Gets the URI of aEditor's document.
 */
static nsIURI* GetDocumentURI(EditorBase* aEditor) {
  MOZ_ASSERT(aEditor);

  Document* doc = aEditor->AsEditorBase()->GetDocument();
  if (NS_WARN_IF(!doc)) {
    return nullptr;
  }

  return doc->GetDocumentURI();
}

static nsILoadContext* GetLoadContext(nsIEditor* aEditor) {
  Document* doc = aEditor->AsEditorBase()->GetDocument();
  if (NS_WARN_IF(!doc)) {
    return nullptr;
  }

  return doc->GetLoadContext();
}

static nsCString DictionariesToString(
    const nsTArray<nsCString>& aDictionaries) {
  nsCString asString;
  for (const auto& dictionary : aDictionaries) {
    asString.Append(dictionary);
    asString.Append(',');
  }
  return asString;
}

static void StringToDictionaries(const nsCString& aString,
                                 nsTArray<nsCString>& aDictionaries) {
  nsTArray<nsCString> asDictionaries;
  for (const nsACString& token :
       nsCCharSeparatedTokenizer(aString, ',').ToRange()) {
    if (token.IsEmpty()) {
      continue;
    }
    aDictionaries.AppendElement(token);
  }
}

/**
 * Fetches the dictionary stored in content prefs and maintains state during the
 * fetch, which is asynchronous.
 */
class DictionaryFetcher final : public nsIContentPrefCallback2 {
 public:
  NS_DECL_ISUPPORTS

  DictionaryFetcher(EditorSpellCheck* aSpellCheck,
                    nsIEditorSpellCheckCallback* aCallback, uint32_t aGroup)
      : mCallback(aCallback), mGroup(aGroup), mSpellCheck(aSpellCheck) {}

  NS_IMETHOD Fetch(nsIEditor* aEditor);

  NS_IMETHOD HandleResult(nsIContentPref* aPref) override {
    nsCOMPtr<nsIVariant> value;
    nsresult rv = aPref->GetValue(getter_AddRefs(value));
    NS_ENSURE_SUCCESS(rv, rv);
    nsCString asString;
    value->GetAsACString(asString);
    StringToDictionaries(asString, mDictionaries);
    return NS_OK;
  }

  NS_IMETHOD HandleCompletion(uint16_t reason) override {
    mSpellCheck->DictionaryFetched(this);
    return NS_OK;
  }

  NS_IMETHOD HandleError(nsresult error) override { return NS_OK; }

  nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
  uint32_t mGroup;
  RefPtr<nsAtom> mRootContentLang;
  RefPtr<nsAtom> mRootDocContentLang;
  nsTArray<nsCString> mDictionaries;

 private:
  ~DictionaryFetcher() {}

  RefPtr<EditorSpellCheck> mSpellCheck;
};

NS_IMPL_ISUPPORTS(DictionaryFetcher, nsIContentPrefCallback2)

class ContentPrefInitializerRunnable final : public Runnable {
 public:
  ContentPrefInitializerRunnable(nsIEditor* aEditor,
                                 nsIContentPrefCallback2* aCallback)
      : Runnable("ContentPrefInitializerRunnable"),
        mEditorBase(aEditor->AsEditorBase()),
        mCallback(aCallback) {}

  NS_IMETHOD Run() override {
    if (mEditorBase->Destroyed()) {
      mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
      return NS_OK;
    }

    nsCOMPtr<nsIContentPrefService2> contentPrefService =
        do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
    if (NS_WARN_IF(!contentPrefService)) {
      mCallback->HandleError(NS_ERROR_NOT_AVAILABLE);
      return NS_OK;
    }

    nsCOMPtr<nsIURI> docUri = GetDocumentURI(mEditorBase);
    if (NS_WARN_IF(!docUri)) {
      mCallback->HandleError(NS_ERROR_FAILURE);
      return NS_OK;
    }

    nsAutoCString docUriSpec;
    nsresult rv = docUri->GetSpec(docUriSpec);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      mCallback->HandleError(rv);
      return NS_OK;
    }

    rv = contentPrefService->GetByDomainAndName(
        NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
        GetLoadContext(mEditorBase), mCallback);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      mCallback->HandleError(rv);
      return NS_OK;
    }
    return NS_OK;
  }

 private:
  RefPtr<EditorBase> mEditorBase;
  nsCOMPtr<nsIContentPrefCallback2> mCallback;
};

NS_IMETHODIMP
DictionaryFetcher::Fetch(nsIEditor* aEditor) {
  NS_ENSURE_ARG_POINTER(aEditor);

  nsCOMPtr<nsIRunnable> runnable =
      new ContentPrefInitializerRunnable(aEditor, this);
  NS_DispatchToCurrentThreadQueue(runnable.forget(), 1000,
                                  EventQueuePriority::Idle);

  return NS_OK;
}

/**
 * Stores the current dictionary for aEditor's document URL.
 */
static nsresult StoreCurrentDictionaries(
    EditorBase* aEditorBase, const nsTArray<nsCString>& aDictionaries) {
  NS_ENSURE_ARG_POINTER(aEditorBase);

  nsresult rv;

  nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
  if (NS_WARN_IF(!docUri)) {
    return NS_ERROR_FAILURE;
  }

  nsAutoCString docUriSpec;
  rv = docUri->GetSpec(docUriSpec);
  NS_ENSURE_SUCCESS(rv, rv);

  RefPtr<nsVariant> prefValue = new nsVariant();

  nsCString asString = DictionariesToString(aDictionaries);
  prefValue->SetAsAString(NS_ConvertUTF8toUTF16(asString));

  nsCOMPtr<nsIContentPrefService2> contentPrefService =
      do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
  NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);

  return contentPrefService->Set(NS_ConvertUTF8toUTF16(docUriSpec),
                                 CPS_PREF_NAME, prefValue,
                                 GetLoadContext(aEditorBase), nullptr);
}

/**
 * Forgets the current dictionary stored for aEditor's document URL.
 */
static nsresult ClearCurrentDictionaries(EditorBase* aEditorBase) {
  NS_ENSURE_ARG_POINTER(aEditorBase);

  nsresult rv;

  nsCOMPtr<nsIURI> docUri = GetDocumentURI(aEditorBase);
  if (NS_WARN_IF(!docUri)) {
    return NS_ERROR_FAILURE;
  }

  nsAutoCString docUriSpec;
  rv = docUri->GetSpec(docUriSpec);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIContentPrefService2> contentPrefService =
      do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID);
  NS_ENSURE_TRUE(contentPrefService, NS_ERROR_NOT_INITIALIZED);

  return contentPrefService->RemoveByDomainAndName(
      NS_ConvertUTF8toUTF16(docUriSpec), CPS_PREF_NAME,
      GetLoadContext(aEditorBase), nullptr);
}

NS_IMPL_CYCLE_COLLECTING_ADDREF(EditorSpellCheck)
NS_IMPL_CYCLE_COLLECTING_RELEASE(EditorSpellCheck)

NS_INTERFACE_MAP_BEGIN(EditorSpellCheck)
  NS_INTERFACE_MAP_ENTRY(nsIEditorSpellCheck)
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIEditorSpellCheck)
  NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(EditorSpellCheck)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTION(EditorSpellCheck, mEditor, mSpellChecker)

EditorSpellCheck::EditorSpellCheck()
    : mTxtSrvFilterType(0),
      mSuggestedWordIndex(0),
      mDictionaryIndex(0),
      mDictionaryFetcherGroup(0),
      mUpdateDictionaryRunning(false) {}

EditorSpellCheck::~EditorSpellCheck() {
  // Make sure we blow the spellchecker away, just in
  // case it hasn't been destroyed already.
  mSpellChecker = nullptr;
}

mozSpellChecker* EditorSpellCheck::GetSpellChecker() { return mSpellChecker; }

// The problem is that if the spell checker does not exist, we can not tell
// which dictionaries are installed. This function works around the problem,
// allowing callers to ask if we can spell check without actually doing so (and
// enabling or disabling UI as necessary). This just creates a spellcheck
// object if needed and asks it for the dictionary list.
NS_IMETHODIMP
EditorSpellCheck::CanSpellCheck(bool* aCanSpellCheck) {
  RefPtr<mozSpellChecker> spellChecker = mSpellChecker;
  if (!spellChecker) {
    spellChecker = mozSpellChecker::Create();
    MOZ_ASSERT(spellChecker);
  }
  nsTArray<nsCString> dictList;
  nsresult rv = spellChecker->GetDictionaryList(&dictList);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  *aCanSpellCheck = !dictList.IsEmpty();
  return NS_OK;
}

// Instances of this class can be used as either runnables or RAII helpers.
class CallbackCaller final : public Runnable {
 public:
  explicit CallbackCaller(nsIEditorSpellCheckCallback* aCallback)
      : mozilla::Runnable("CallbackCaller"), mCallback(aCallback) {}

  ~CallbackCaller() { Run(); }

  NS_IMETHOD Run() override {
    if (mCallback) {
      mCallback->EditorSpellCheckDone();
      mCallback = nullptr;
    }
    return NS_OK;
  }

 private:
  nsCOMPtr<nsIEditorSpellCheckCallback> mCallback;
};

NS_IMETHODIMP
EditorSpellCheck::InitSpellChecker(nsIEditor* aEditor,
                                   bool aEnableSelectionChecking,
                                   nsIEditorSpellCheckCallback* aCallback) {
  NS_ENSURE_TRUE(aEditor, NS_ERROR_NULL_POINTER);
  mEditor = aEditor->AsEditorBase();

  RefPtr<Document> doc = mEditor->GetDocument();
  if (NS_WARN_IF(!doc)) {
    return NS_ERROR_FAILURE;
  }

  nsresult rv;

  // We can spell check with any editor type
  RefPtr<TextServicesDocument> textServicesDocument =
      new TextServicesDocument();
  textServicesDocument->SetFilterType(mTxtSrvFilterType);

  // EditorBase::AddEditActionListener() needs to access mSpellChecker and
  // mSpellChecker->GetTextServicesDocument().  Therefore, we need to
  // initialize them before calling TextServicesDocument::InitWithEditor()
  // since it calls EditorBase::AddEditActionListener().
  mSpellChecker = mozSpellChecker::Create();
  MOZ_ASSERT(mSpellChecker);
  rv = mSpellChecker->SetDocument(textServicesDocument, true);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Pass the editor to the text services document
  rv = textServicesDocument->InitWithEditor(aEditor);
  NS_ENSURE_SUCCESS(rv, rv);

  if (aEnableSelectionChecking) {
    // Find out if the section is collapsed or not.
    // If it isn't, we want to spellcheck just the selection.

    RefPtr<Selection> selection;
    aEditor->GetSelection(getter_AddRefs(selection));
    if (NS_WARN_IF(!selection)) {
      return NS_ERROR_FAILURE;
    }

    if (selection->RangeCount()) {
      RefPtr<const nsRange> range = selection->GetRangeAt(0);
      NS_ENSURE_STATE(range);

      if (!range->Collapsed()) {
        // We don't want to touch the range in the selection,
        // so create a new copy of it.
        RefPtr<StaticRange> staticRange =
            StaticRange::Create(range, IgnoreErrors());
        if (NS_WARN_IF(!staticRange)) {
          return NS_ERROR_FAILURE;
        }

        // Make sure the new range spans complete words.
        rv = textServicesDocument->ExpandRangeToWordBoundaries(staticRange);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }

        // Now tell the text services that you only want
        // to iterate over the text in this range.
        rv = textServicesDocument->SetExtent(staticRange);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }
      }
    }
  }
  // do not fail if UpdateCurrentDictionary fails because this method may
  // succeed later.
  rv = UpdateCurrentDictionary(aCallback);
  if (NS_FAILED(rv) && aCallback) {
    // However, if it does fail, we still need to call the callback since we
    // discard the failure.  Do it asynchronously so that the caller is always
    // guaranteed async behavior.
    RefPtr<CallbackCaller> caller = new CallbackCaller(aCallback);
    rv = doc->Dispatch(caller.forget());
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

NS_IMETHODIMP
EditorSpellCheck::GetNextMisspelledWord(nsAString& aNextMisspelledWord) {
  MOZ_LOG(sEditorSpellChecker, LogLevel::Debug, ("%s", __FUNCTION__));

  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  DeleteSuggestedWordList();
  // Beware! This may flush notifications via synchronous
  // ScrollSelectionIntoView.
  RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
  return spellChecker->NextMisspelledWord(aNextMisspelledWord,
                                          mSuggestedWordList);
}

NS_IMETHODIMP
EditorSpellCheck::GetSuggestedWord(nsAString& aSuggestedWord) {
  // XXX This is buggy if mSuggestedWordList.Length() is over INT32_MAX.
  if (mSuggestedWordIndex < static_cast<int32_t>(mSuggestedWordList.Length())) {
    aSuggestedWord = mSuggestedWordList[mSuggestedWordIndex];
    mSuggestedWordIndex++;
  } else {
    // A blank string signals that there are no more strings
    aSuggestedWord.Truncate();
  }
  return NS_OK;
}

NS_IMETHODIMP
EditorSpellCheck::CheckCurrentWord(const nsAString& aSuggestedWord,
                                   bool* aIsMisspelled) {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  DeleteSuggestedWordList();
  return mSpellChecker->CheckWord(aSuggestedWord, aIsMisspelled,
                                  &mSuggestedWordList);
}

NS_IMETHODIMP
EditorSpellCheck::Suggest(const nsAString& aSuggestedWord, uint32_t aCount,
                          JSContext* aCx, Promise** aPromise) {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
  if (NS_WARN_IF(!globalObject)) {
    return NS_ERROR_UNEXPECTED;
  }

  ErrorResult result;
  RefPtr<Promise> promise = Promise::Create(globalObject, result);
  if (NS_WARN_IF(result.Failed())) {
    return result.StealNSResult();
  }

  mSpellChecker->Suggest(aSuggestedWord, aCount)
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          [promise](const CopyableTArray<nsString>& aSuggestions) {
            promise->MaybeResolve(aSuggestions);
          },
          [promise](nsresult aError) {
            promise->MaybeReject(NS_ERROR_FAILURE);
          });

  promise.forget(aPromise);
  return NS_OK;
}

RefPtr<CheckWordPromise> EditorSpellCheck::CheckCurrentWordsNoSuggest(
    const nsTArray<nsString>& aSuggestedWords) {
  if (NS_WARN_IF(!mSpellChecker)) {
    return CheckWordPromise::CreateAndReject(NS_ERROR_NOT_INITIALIZED,
                                             __func__);
  }

  return mSpellChecker->CheckWords(aSuggestedWords);
}

NS_IMETHODIMP
EditorSpellCheck::ReplaceWord(const nsAString& aMisspelledWord,
                              const nsAString& aReplaceWord,
                              bool aAllOccurrences) {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  RefPtr<mozSpellChecker> spellChecker(mSpellChecker);
  return spellChecker->Replace(aMisspelledWord, aReplaceWord, aAllOccurrences);
}

NS_IMETHODIMP
EditorSpellCheck::IgnoreWordAllOccurrences(const nsAString& aWord) {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  return mSpellChecker->IgnoreAll(aWord);
}

NS_IMETHODIMP
EditorSpellCheck::GetPersonalDictionary() {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  // We can spell check with any editor type
  mDictionaryList.Clear();
  mDictionaryIndex = 0;
  return mSpellChecker->GetPersonalDictionary(&mDictionaryList);
}

NS_IMETHODIMP
EditorSpellCheck::GetPersonalDictionaryWord(nsAString& aDictionaryWord) {
  // XXX This is buggy if mDictionaryList.Length() is over INT32_MAX.
  if (mDictionaryIndex < static_cast<int32_t>(mDictionaryList.Length())) {
    aDictionaryWord = mDictionaryList[mDictionaryIndex];
    mDictionaryIndex++;
  } else {
    // A blank string signals that there are no more strings
    aDictionaryWord.Truncate();
  }

  return NS_OK;
}

NS_IMETHODIMP
EditorSpellCheck::AddWordToDictionary(const nsAString& aWord) {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  return mSpellChecker->AddWordToPersonalDictionary(aWord);
}

NS_IMETHODIMP
EditorSpellCheck::RemoveWordFromDictionary(const nsAString& aWord) {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  return mSpellChecker->RemoveWordFromPersonalDictionary(aWord);
}

NS_IMETHODIMP
EditorSpellCheck::GetDictionaryList(nsTArray<nsCString>& aList) {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  return mSpellChecker->GetDictionaryList(&aList);
}

NS_IMETHODIMP
EditorSpellCheck::GetCurrentDictionaries(nsTArray<nsCString>& aDictionaries) {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);
  return mSpellChecker->GetCurrentDictionaries(aDictionaries);
}

NS_IMETHODIMP
EditorSpellCheck::SetCurrentDictionaries(
    const nsTArray<nsCString>& aDictionaries, JSContext* aCx,
    Promise** aPromise) {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  RefPtr<EditorSpellCheck> kungFuDeathGrip = this;

  // The purpose of mUpdateDictionaryRunning is to avoid doing all of this if
  // UpdateCurrentDictionary's helper method DictionaryFetched, which calls us,
  // is on the stack. In other words: Only do this, if the user manually
  // selected a dictionary to use.
  if (!mUpdateDictionaryRunning) {
    // Ignore pending dictionary fetchers by increasing this number.
    mDictionaryFetcherGroup++;

    uint32_t flags = 0;
    mEditor->GetFlags(&flags);
    if (!(flags & nsIEditor::eEditorMailMask)) {
      bool contentPrefMatchesUserPref = true;
      // Check if aDictionaries has the same languages as mPreferredLangs.
      if (!aDictionaries.IsEmpty()) {
        if (aDictionaries.Length() != mPreferredLangs.Length()) {
          contentPrefMatchesUserPref = false;
        } else {
          for (const auto& dictName : aDictionaries) {
            if (mPreferredLangs.IndexOf(dictName) ==
                nsTArray<nsCString>::NoIndex) {
              contentPrefMatchesUserPref = false;
              break;
            }
          }
        }
      }
      if (!contentPrefMatchesUserPref) {
        // When user sets dictionary manually, we store this value associated
        // with editor url, if it doesn't match the document language exactly.
        // For example on "en" sites, we need to store "en-GB", otherwise
        // the language might jump back to en-US although the user explicitly
        // chose otherwise.
        StoreCurrentDictionaries(mEditor, aDictionaries);
#ifdef DEBUG_DICT
        printf("***** Writing content preferences for |%s|\n",
               DictionariesToString(aDictionaries).Data());
#endif
      } else {
        // If user sets a dictionary matching the language defined by
        // document, we consider content pref has been canceled, and we clear
        // it.
        ClearCurrentDictionaries(mEditor);
#ifdef DEBUG_DICT
        printf("***** Clearing content preferences for |%s|\n",
               DictionariesToString(aDictionaries).Data());
#endif
      }

      // Also store it in as a preference, so we can use it as a fallback.
      // We don't want this for mail composer because it uses
      // "spellchecker.dictionary" as a preference.
      //
      // XXX: Prefs can only be set in the parent process, so this condition is
      // necessary to stop libpref from throwing errors. But this should
      // probably be handled in a better way.
      if (XRE_IsParentProcess()) {
        nsCString asString = DictionariesToString(aDictionaries);
        Preferences::SetCString("spellchecker.dictionary", asString);
#ifdef DEBUG_DICT
        printf("***** Possibly storing spellchecker.dictionary |%s|\n",
               asString.Data());
#endif
      }
    } else {
      MOZ_ASSERT(flags & nsIEditor::eEditorMailMask);
      // Since the mail editor can only influence the language selection by the
      // html lang attribute, set the content-language document to persist
      // multi language selections.
      // XXX Why doesn't here use the document of the editor directly?
      nsCOMPtr<nsIContent> anonymousDivOrEditingHost;
      if (HTMLEditor* htmlEditor = mEditor->GetAsHTMLEditor()) {
        anonymousDivOrEditingHost = htmlEditor->ComputeEditingHost();
      } else {
        anonymousDivOrEditingHost = mEditor->GetRoot();
      }
      RefPtr<Document> ownerDoc = anonymousDivOrEditingHost->OwnerDoc();
      Document* parentDoc = ownerDoc->GetInProcessParentDocument();
      if (parentDoc) {
        parentDoc->SetHeaderData(
            nsGkAtoms::headerContentLanguage,
            NS_ConvertUTF8toUTF16(DictionariesToString(aDictionaries)));
      }
    }
  }

  nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
  if (NS_WARN_IF(!globalObject)) {
    return NS_ERROR_UNEXPECTED;
  }

  ErrorResult result;
  RefPtr<Promise> promise = Promise::Create(globalObject, result);
  if (NS_WARN_IF(result.Failed())) {
    return result.StealNSResult();
  }

  mSpellChecker->SetCurrentDictionaries(aDictionaries)
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          [promise]() { promise->MaybeResolveWithUndefined(); },
          [promise](nsresult aError) {
            promise->MaybeReject(NS_ERROR_FAILURE);
          });

  promise.forget(aPromise);
  return NS_OK;
}

NS_IMETHODIMP
EditorSpellCheck::UninitSpellChecker() {
  NS_ENSURE_TRUE(mSpellChecker, NS_ERROR_NOT_INITIALIZED);

  // Cleanup - kill the spell checker
  DeleteSuggestedWordList();
  mDictionaryList.Clear();
  mDictionaryIndex = 0;
  mDictionaryFetcherGroup++;
  mSpellChecker = nullptr;
  return NS_OK;
}

NS_IMETHODIMP
EditorSpellCheck::SetFilterType(uint32_t aFilterType) {
  mTxtSrvFilterType = aFilterType;
  return NS_OK;
}

nsresult EditorSpellCheck::DeleteSuggestedWordList() {
  mSuggestedWordList.Clear();
  mSuggestedWordIndex = 0;
  return NS_OK;
}

NS_IMETHODIMP
EditorSpellCheck::UpdateCurrentDictionary(
    nsIEditorSpellCheckCallback* aCallback) {
  if (NS_WARN_IF(!mSpellChecker)) {
    return NS_ERROR_NOT_INITIALIZED;
  }

  nsresult rv;

  RefPtr<EditorSpellCheck> kungFuDeathGrip = this;

  // Get language with html5 algorithm
  const RefPtr<Element> rootEditableElement =
      [](const EditorBase& aEditorBase) -> Element* {
    if (!aEditorBase.IsHTMLEditor()) {
      return aEditorBase.GetRoot();
    }
    if (aEditorBase.IsMailEditor()) {
      // Shouldn't run spellcheck in a mail editor without focus
      // (bug 1507543)
      // XXX Why doesn't here use the document of the editor directly?
      Element* const editingHost =
          aEditorBase.AsHTMLEditor()->ComputeEditingHost();
      if (!editingHost) {
        return nullptr;
      }
      // Try to get topmost document's document element for embedded mail
      // editor (bug 967494)
      Document* parentDoc =
          editingHost->OwnerDoc()->GetInProcessParentDocument();
      if (!parentDoc) {
        return editingHost;
      }
      return parentDoc->GetDocumentElement();
    }
    return aEditorBase.AsHTMLEditor()->GetFocusedElement();
  }(*mEditor);

  if (!rootEditableElement) {
    return NS_ERROR_FAILURE;
  }

  RefPtr<DictionaryFetcher> fetcher =
      new DictionaryFetcher(this, aCallback, mDictionaryFetcherGroup);
  fetcher->mRootContentLang = rootEditableElement->GetLang();
  RefPtr<Document> doc = rootEditableElement->GetComposedDoc();
  NS_ENSURE_STATE(doc);
  fetcher->mRootDocContentLang = doc->GetContentLanguage();

  rv = fetcher->Fetch(mEditor);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

// Helper function that iterates over the list of dictionaries and sets the one
// that matches based on a given comparison type.
bool EditorSpellCheck::BuildDictionaryList(const nsACString& aDictName,
                                           const nsTArray<nsCString>& aDictList,
                                           enum dictCompare aCompareType,
                                           nsTArray<nsCString>& aOutList) {
  for (const auto& dictStr : aDictList) {
    bool equals = false;
    switch (aCompareType) {
      case DICT_NORMAL_COMPARE:
        equals = aDictName.Equals(dictStr);
        break;
      case DICT_COMPARE_CASE_INSENSITIVE:
        equals = aDictName.Equals(dictStr, nsCaseInsensitiveCStringComparator);
        break;
      case DICT_COMPARE_DASHMATCH:
        equals = nsStyleUtil::DashMatchCompare(
            NS_ConvertUTF8toUTF16(dictStr), NS_ConvertUTF8toUTF16(aDictName),
            nsCaseInsensitiveStringComparator);
        break;
    }
    if (equals) {
      // Avoid adding duplicates to aOutList.
      if (aOutList.IndexOf(dictStr) == nsTArray<nsCString>::NoIndex) {
        aOutList.AppendElement(dictStr);
      }
#ifdef DEBUG_DICT
      printf("***** Trying |%s|.\n", dictStr.get());
#endif
      // We always break here. We tried to set the dictionary to an existing
      // dictionary from the list. This must work, if it doesn't, there is
      // no point trying another one.
      return true;
    }
  }
  return false;
}

nsresult EditorSpellCheck::DictionaryFetched(DictionaryFetcher* aFetcher) {
  MOZ_ASSERT(aFetcher);
  RefPtr<EditorSpellCheck> kungFuDeathGrip = this;

  BeginUpdateDictionary();

  if (aFetcher->mGroup < mDictionaryFetcherGroup) {
    // SetCurrentDictionary was called after the fetch started.  Don't overwrite
    // that dictionary with the fetched one.
    EndUpdateDictionary();
    if (aFetcher->mCallback) {
      aFetcher->mCallback->EditorSpellCheckDone();
    }
    return NS_OK;
  }

  /*
   * We try to derive the dictionary to use based on the following priorities:
   * 1) Content preference, so the language the user set for the site before.
   *    (Introduced in bug 678842 and corrected in bug 717433.)
   * 2) Language set by the website, or any other dictionary that partly
   *    matches that. (Introduced in bug 338427.)
   *    Eg. if the website is "en-GB", a user who only has "en-US" will get
   *    that. If the website is generic "en", the user will get one of the
   *    "en-*" installed. If application locale or system locale is "en-*",
   *    we get it. If others, it is (almost) random.
   *    However, we prefer what is stored in "spellchecker.dictionary",
   *    so if the user chose "en-AU" before, they will get "en-AU" on a plain
   *    "en" site. (Introduced in bug 682564.)
   *    If the site has multiple languages declared in its Content-Language
   *    header and there is no more specific lang tag in HTML, we try to
   *    enable a dictionary for every content language.
   * 3) The value of "spellchecker.dictionary" which reflects a previous
   *    language choice of the user (on another site).
   *    (This was the original behaviour before the aforementioned bugs
   *    landed).
   * 4) The user's locale.
   * 5) Use the current dictionary that is currently set.
   * 6) The content of the "LANG" environment variable (if set).
   * 7) The first spell check dictionary installed.
   */

  // Get the language from the element or its closest parent according to:
  // https://html.spec.whatwg.org/#attr-lang
  // This is used in SetCurrentDictionaries.
  nsCString contentLangs;
  // Reset mPreferredLangs so we only get the current state.
  mPreferredLangs.Clear();
  if (aFetcher->mRootContentLang) {
    aFetcher->mRootContentLang->ToUTF8String(contentLangs);
  }
#ifdef DEBUG_DICT
  printf("***** mPreferredLangs (element) |%s|\n", contentLangs.get());
#endif
  if (!contentLangs.IsEmpty()) {
    mPreferredLangs.AppendElement(contentLangs);
  } else {
    // If no luck, try the "Content-Language" header.
    if (aFetcher->mRootDocContentLang) {
      aFetcher->mRootDocContentLang->ToUTF8String(contentLangs);
    }
#ifdef DEBUG_DICT
    printf("***** mPreferredLangs (content-language) |%s|\n",
           contentLangs.get());
#endif
    StringToDictionaries(contentLangs, mPreferredLangs);
  }

  // We obtain a list of available dictionaries.
  AutoTArray<nsCString, 8> dictList;
  nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    EndUpdateDictionary();
    if (aFetcher->mCallback) {
      aFetcher->mCallback->EditorSpellCheckDone();
    }
    return rv;
  }

  // Priority 1:
  // If we successfully fetched a dictionary from content prefs, do not go
  // further. Use this exact dictionary.
  // Don't use content preferences for editor with eEditorMailMask flag.
  nsAutoCString dictName;
  uint32_t flags;
  mEditor->GetFlags(&flags);
  if (!(flags & nsIEditor::eEditorMailMask)) {
    if (!aFetcher->mDictionaries.IsEmpty()) {
      RefPtr<EditorSpellCheck> self = this;
      RefPtr<DictionaryFetcher> fetcher = aFetcher;
      mSpellChecker->SetCurrentDictionaries(aFetcher->mDictionaries)
          ->Then(
              GetMainThreadSerialEventTarget(), __func__,
              [self, fetcher]() {
#ifdef DEBUG_DICT
                printf("***** Assigned from content preferences |%s|\n",
                       DictionariesToString(fetcher->mDictionaries).Data());
#endif
                // We take an early exit here, so let's not forget to clear
                // the word list.
                self->DeleteSuggestedWordList();

                self->EndUpdateDictionary();
                if (fetcher->mCallback) {
                  fetcher->mCallback->EditorSpellCheckDone();
                }
              },
              [self, fetcher](nsresult aError) {
                if (aError == NS_ERROR_ABORT) {
                  return;
                }
                // May be dictionary was uninstalled ?
                // Clear the content preference and continue.
                ClearCurrentDictionaries(self->mEditor);

                // Priority 2 or later will handled by the following
                self->SetFallbackDictionary(fetcher);
              });
      return NS_OK;
    }
  }
  SetFallbackDictionary(aFetcher);
  return NS_OK;
}

void EditorSpellCheck::SetDictionarySucceeded(DictionaryFetcher* aFetcher) {
  DeleteSuggestedWordList();
  EndUpdateDictionary();
  if (aFetcher->mCallback) {
    aFetcher->mCallback->EditorSpellCheckDone();
  }
}

void EditorSpellCheck::SetFallbackDictionary(DictionaryFetcher* aFetcher) {
  MOZ_ASSERT(mUpdateDictionaryRunning);

  AutoTArray<nsCString, 6> tryDictList;

  // We obtain a list of available dictionaries.
  AutoTArray<nsCString, 8> dictList;
  nsresult rv = mSpellChecker->GetDictionaryList(&dictList);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    EndUpdateDictionary();
    if (aFetcher->mCallback) {
      aFetcher->mCallback->EditorSpellCheckDone();
    }
    return;
  }

  // Priority 2:
  // After checking the content preferences, we use the languages of the element
  // or document.

  // Get the preference value.
  nsAutoCString prefDictionariesAsString;
  Preferences::GetLocalizedCString("spellchecker.dictionary",
                                   prefDictionariesAsString);
  nsTArray<nsCString> prefDictionaries;
  StringToDictionaries(prefDictionariesAsString, prefDictionaries);

  nsAutoCString appLocaleStr;
  // We pick one dictionary for every language that the element or document
  // indicates it contains.
  for (const auto& dictName : mPreferredLangs) {
    // RFC 5646 explicitly states that matches should be case-insensitive.
    if (BuildDictionaryList(dictName, dictList, DICT_COMPARE_CASE_INSENSITIVE,
                            tryDictList)) {
#ifdef DEBUG_DICT
      printf("***** Trying from element/doc |%s| \n", dictName.get());
#endif
      continue;
    }

    // Required dictionary was not available. Try to get a dictionary
    // matching at least language part of dictName.
    mozilla::intl::Locale loc;
    if (mozilla::intl::LocaleParser::TryParse(dictName, loc).isOk() &&
        loc.Canonicalize().isOk()) {
      Span<const char> language = loc.Language().Span();
      nsAutoCString langCode(language.data(), language.size());

      // Try dictionary.spellchecker preference, if it starts with langCode,
      // so we don't just get any random dictionary matching the language.
      bool didAppend = false;
      for (const auto& dictionary : prefDictionaries) {
        if (nsStyleUtil::DashMatchCompare(NS_ConvertUTF8toUTF16(dictionary),
                                          NS_ConvertUTF8toUTF16(langCode),
                                          nsTDefaultStringComparator)) {
#ifdef DEBUG_DICT
          printf(
              "***** Trying preference value |%s| since it matches language "
              "code\n",
              dictionary.Data());
#endif
          if (BuildDictionaryList(dictionary, dictList,
                                  DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
            didAppend = true;
            break;
          }
        }
      }
      if (didAppend) {
        continue;
      }

      // Use the application locale dictionary when the required language
      // equals applocation locale language.
      LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
      if (!appLocaleStr.IsEmpty()) {
        mozilla::intl::Locale appLoc;
        auto result =
            mozilla::intl::LocaleParser::TryParse(appLocaleStr, appLoc);
        if (result.isOk() && appLoc.Canonicalize().isOk() &&
            loc.Language().Span() == appLoc.Language().Span()) {
          if (BuildDictionaryList(appLocaleStr, dictList,
                                  DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
            continue;
          }
        }
      }

      // Use the system locale dictionary when the required language equlas
      // system locale language.
      nsAutoCString sysLocaleStr;
      OSPreferences::GetInstance()->GetSystemLocale(sysLocaleStr);
      if (!sysLocaleStr.IsEmpty()) {
        mozilla::intl::Locale sysLoc;
        auto result =
            mozilla::intl::LocaleParser::TryParse(sysLocaleStr, sysLoc);
        if (result.isOk() && sysLoc.Canonicalize().isOk() &&
            loc.Language().Span() == sysLoc.Language().Span()) {
          if (BuildDictionaryList(sysLocaleStr, dictList,
                                  DICT_COMPARE_CASE_INSENSITIVE, tryDictList)) {
            continue;
          }
        }
      }

      // Use any dictionary with the required language.
#ifdef DEBUG_DICT
      printf("***** Trying to find match for language code |%s|\n",
             langCode.get());
#endif
      BuildDictionaryList(langCode, dictList, DICT_COMPARE_DASHMATCH,
                          tryDictList);
    }
  }

  RefPtr<EditorSpellCheck> self = this;
  RefPtr<DictionaryFetcher> fetcher = aFetcher;
  RefPtr<GenericPromise> promise;

  if (tryDictList.IsEmpty()) {
    // Proceed to priority 3 if the list of dictionaries is empty.
    promise = GenericPromise::CreateAndReject(NS_ERROR_INVALID_ARG, __func__);
  } else {
    promise = mSpellChecker->SetCurrentDictionaries(tryDictList);
  }

  // If an error was thrown while setting the dictionary, just
  // fail silently so that the spellchecker dialog is allowed to come
  // up. The user can manually reset the language to their choice on
  // the dialog if it is wrong.
  promise->Then(
      GetMainThreadSerialEventTarget(), __func__,
      [self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
      [prefDictionaries = prefDictionaries.Clone(), dictList = dictList.Clone(),
       self, fetcher]() {
        // Build tryDictList with dictionaries for priorities 4 through 7.
        // We'll use this list if there is no user preference or trying
        // the user preference fails.
        AutoTArray<nsCString, 6> tryDictList;

        // Priority 4:
        // As next fallback, try the current locale.
        nsAutoCString appLocaleStr;
        LocaleService::GetInstance()->GetAppLocaleAsBCP47(appLocaleStr);
#ifdef DEBUG_DICT
        printf("***** Trying locale |%s|\n", appLocaleStr.get());
#endif
        self->BuildDictionaryList(appLocaleStr, dictList,
                                  DICT_COMPARE_CASE_INSENSITIVE, tryDictList);

        // Priority 5:
        // If we have a current dictionary and we don't have no item in try
        // list, don't try anything else.
        nsTArray<nsCString> currentDictionaries;
        self->GetCurrentDictionaries(currentDictionaries);
        if (!currentDictionaries.IsEmpty() && tryDictList.IsEmpty()) {
#ifdef DEBUG_DICT
          printf("***** Retrieved current dict |%s|\n",
                 DictionariesToString(currentDictionaries).Data());
#endif
          self->EndUpdateDictionary();
          if (fetcher->mCallback) {
            fetcher->mCallback->EditorSpellCheckDone();
          }
          return;
        }

        // Priority 6:
        // Try to get current dictionary from environment variable LANG.
        // LANG = language[_territory][.charset]
        char* env_lang = getenv("LANG");
        if (env_lang) {
          nsAutoCString lang(env_lang);
          // Strip trailing charset, if there is any.
          int32_t dot_pos = lang.FindChar('.');
          if (dot_pos != -1) {
            lang = Substring(lang, 0, dot_pos);
          }

          int32_t underScore = lang.FindChar('_');
          if (underScore != -1) {
            lang.Replace(underScore, 1, '-');
#ifdef DEBUG_DICT
            printf("***** Trying LANG from environment |%s|\n", lang.get());
#endif
            self->BuildDictionaryList(
                lang, dictList, DICT_COMPARE_CASE_INSENSITIVE, tryDictList);
          }
        }

        // Priority 7:
        // If it does not work, pick the first one.
        if (!dictList.IsEmpty()) {
          self->BuildDictionaryList(dictList[0], dictList, DICT_NORMAL_COMPARE,
                                    tryDictList);
#ifdef DEBUG_DICT
          printf("***** Trying first of list |%s|\n", dictList[0].get());
#endif
        }

        // Priority 3:
        // If the document didn't supply a dictionary or the setting
        // failed, try the user preference next.
        if (!prefDictionaries.IsEmpty()) {
          self->mSpellChecker->SetCurrentDictionaries(prefDictionaries)
              ->Then(
                  GetMainThreadSerialEventTarget(), __func__,
                  [self, fetcher]() { self->SetDictionarySucceeded(fetcher); },
                  // Priority 3 failed, we'll use the list we built of
                  // priorities 4 to 7.
                  [tryDictList = tryDictList.Clone(), self, fetcher]() {
                    self->mSpellChecker
                        ->SetCurrentDictionaryFromList(tryDictList)
                        ->Then(GetMainThreadSerialEventTarget(), __func__,
                               [self, fetcher]() {
                                 self->SetDictionarySucceeded(fetcher);
                               });
                  });
        } else {
          // We don't have a user preference, so we'll try the list we
          // built of priorities 4 to 7.
          self->mSpellChecker->SetCurrentDictionaryFromList(tryDictList)
              ->Then(
                  GetMainThreadSerialEventTarget(), __func__,
                  [self, fetcher]() { self->SetDictionarySucceeded(fetcher); });
        }
      });
}

}  // namespace mozilla