// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/spellchecker/spellcheck_service.h"

#include <algorithm>
#include <iterator>
#include <memory>
#include <set>
#include <utility>

#include "base/check_op.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/supports_user_data.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/timer/elapsed_timer.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/spellchecker/spellcheck_factory.h"
#include "chrome/browser/spellchecker/spellcheck_hunspell_dictionary.h"
#include "chrome/common/chrome_features.h"
#include "components/language/core/browser/pref_names.h"
#include "components/prefs/pref_member.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/spellcheck/browser/pref_names.h"
#include "components/spellcheck/browser/spellcheck_host_metrics.h"
#include "components/spellcheck/browser/spellcheck_platform.h"
#include "components/spellcheck/browser/spelling_service_client.h"
#include "components/spellcheck/common/spellcheck.mojom.h"
#include "components/spellcheck/common/spellcheck_common.h"
#include "components/spellcheck/common/spellcheck_features.h"
#include "components/spellcheck/spellcheck_buildflags.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_features.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
#include "ui/base/l10n/l10n_util.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_features.h"
#endif

#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/spellcheck/browser/windows_spell_checker.h"
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

using content::BrowserThread;

namespace {

SpellcheckService::SpellCheckerBinder& GetSpellCheckerBinderOverride() {
  static base::NoDestructor<SpellcheckService::SpellCheckerBinder> binder;
  return *binder;
}

// Only record spelling-configuration metrics for profiles in which the user
// can configure spelling.
bool RecordSpellingConfigurationMetrics(content::BrowserContext* context) {
  return profiles::IsRegularUserProfile(Profile::FromBrowserContext(context));
}

}  // namespace

// TODO(rlp): I do not like globals, but keeping these for now during
// transition.
// An event used by browser tests to receive status events from this class and
// its derived classes.
base::WaitableEvent* g_status_event = nullptr;
SpellcheckService::EventType g_status_type =
    SpellcheckService::BDICT_NOTINITIALIZED;

SpellcheckService::SpellcheckService(content::BrowserContext* context)
    : context_(context) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  bool defer_spellcheck =
      base::FeatureList::IsEnabled(::features::kDeferSpellcheckInitialization);

  PrefService* prefs = user_prefs::UserPrefs::Get(context);
  pref_change_registrar_.Init(prefs);
  StringListPrefMember dictionaries_pref;
  dictionaries_pref.Init(spellcheck::prefs::kSpellCheckDictionaries, prefs);
  std::string first_of_dictionaries;

#if BUILDFLAG(IS_MAC)
  if (defer_spellcheck) {
    // Defer Cocoa spellchecker initialization to a post-startup idle task to
    // avoid blocking the main thread during critical startup path.
    // This is safe because:
    // 1. Spellcheck is not needed immediately on startup.
    // 2. Renderers that don't need spellcheck (like Top Chrome WebUI and NTP)
    //    are bypassed entirely in `InitForRenderer`, so they won't trigger
    //    early initialization of the service.
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
        base::BindOnce(&spellcheck_platform::GetSpellCheckerLanguage),
        base::BindOnce(&SpellcheckService::InitMacDeferredSpellcheck,
                       weak_ptr_factory_.GetWeakPtr()));
  } else {
    InitializePlatformLanguageMacWithLanguage(
        spellcheck_platform::GetSpellCheckerLanguage());
  }
#elif BUILDFLAG(IS_ANDROID)
  // Ensure that the renderer always knows the platform spellchecking
  // language. This language is used for initialization of the text iterator.
  // If the iterator is not initialized, then the context menu does not show
  // spellcheck suggestions.
  dictionaries_pref.SetValue(std::vector<std::string>(
      1, spellcheck_platform::GetSpellCheckerLanguage()));
#else
  // Migrate preferences from single-language to multi-language schema.
  StringPrefMember single_dictionary_pref;
  single_dictionary_pref.Init(spellcheck::prefs::kSpellCheckDictionary, prefs);
  std::string single_dictionary = single_dictionary_pref.GetValue();

  if (!dictionaries_pref.GetValue().empty())
    first_of_dictionaries = dictionaries_pref.GetValue().front();

  if (first_of_dictionaries.empty() && !single_dictionary.empty()) {
    first_of_dictionaries = single_dictionary;
    dictionaries_pref.SetValue(
        std::vector<std::string>(1, first_of_dictionaries));
  }

  single_dictionary_pref.SetValue("");
#endif  // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  if (!spellcheck::UseBrowserSpellChecker()) {
    // A user may have disabled the Windows spellcheck feature after adding
    // non-Hunspell supported languages on the language settings page. Remove
    // preferences for non-Hunspell languages so that there is no attempt to
    // load a non-existent Hunspell dictionary, and so that Hunspell
    // spellchecking isn't broken because of the failed load.
    ScopedListPrefUpdate update(prefs,
                                spellcheck::prefs::kSpellCheckDictionaries);
    update->EraseIf([](const base::Value& entry) {
      return spellcheck::GetCorrespondingSpellCheckLanguage(entry.GetString())
          .empty();
    });
  }
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

  pref_change_registrar_.Add(
      spellcheck::prefs::kSpellCheckDictionaries,
      base::BindRepeating(&SpellcheckService::OnSpellCheckDictionariesChanged,
                          base::Unretained(this)));
  pref_change_registrar_.Add(
      spellcheck::prefs::kSpellCheckForcedDictionaries,
      base::BindRepeating(&SpellcheckService::OnSpellCheckDictionariesChanged,
                          base::Unretained(this)));
  pref_change_registrar_.Add(
      spellcheck::prefs::kSpellCheckBlocklistedDictionaries,
      base::BindRepeating(&SpellcheckService::OnSpellCheckDictionariesChanged,
                          base::Unretained(this)));
  pref_change_registrar_.Add(
      spellcheck::prefs::kSpellCheckUseSpellingService,
      base::BindRepeating(&SpellcheckService::OnUseSpellingServiceChanged,
                          base::Unretained(this)));
  pref_change_registrar_.Add(
      language::prefs::kAcceptLanguages,
      base::BindRepeating(&SpellcheckService::OnAcceptLanguagesChanged,
                          base::Unretained(this)));
  pref_change_registrar_.Add(
      spellcheck::prefs::kSpellCheckEnable,
      base::BindRepeating(&SpellcheckService::InitForAllRenderers,
                          base::Unretained(this)));

  custom_dictionary_ =
      std::make_unique<SpellcheckCustomDictionary>(context_->GetPath());
  custom_dictionary_->AddObserver(this);

  // Determine if dictionary initialization should be deferred or skipped in
  // this constructor.
  bool run_custom_dict_load = true;
  bool run_hunspell_init = true;

#if BUILDFLAG(IS_MAC)
  if (defer_spellcheck) {
    run_custom_dict_load = false;
    run_hunspell_init = false;
  }
#endif

  // 1. Load custom dictionary.
  if (run_custom_dict_load) {
    if (defer_spellcheck) {
      content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
          ->PostTask(FROM_HERE,
                     base::BindOnce(&SpellcheckCustomDictionary::Load,
                                    custom_dictionary_->GetWeakPtr()));
    } else {
      custom_dictionary_->Load();
    }
  }

#if BUILDFLAG(IS_WIN)
  if (spellcheck::UseBrowserSpellChecker()) {
    // If initialization of the spellcheck service is on-demand, it is up to the
    // instantiator of the spellcheck service to call InitializeDictionaries
    // with a callback.
    return;
  }
#endif  // BUILDFLAG(IS_WIN)

  // 2. Initialize Hunspell dictionaries.
  if (run_hunspell_init) {
    if (defer_spellcheck) {
      content::GetUIThreadTaskRunner({base::TaskPriority::BEST_EFFORT})
          ->PostTask(FROM_HERE,
                     base::BindOnce(&SpellcheckService::InitializeDictionaries,
                                    weak_ptr_factory_.GetWeakPtr(),
                                    base::DoNothing()));
    } else {
      InitializeDictionaries(base::DoNothing());
    }
  }
}

SpellcheckService::~SpellcheckService() {
  // Remove pref observers
  pref_change_registrar_.RemoveAll();
}

base::WeakPtr<SpellcheckService> SpellcheckService::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

#if BUILDFLAG(IS_MAC)
void SpellcheckService::InitializePlatformLanguageMacWithLanguage(
    const std::string& platform_lang) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  PrefService* prefs = user_prefs::UserPrefs::Get(context_);
  StringListPrefMember dictionaries_pref;
  dictionaries_pref.Init(spellcheck::prefs::kSpellCheckDictionaries, prefs);

  // Ensure that the renderer always knows the platform spellchecking
  // language. This language is used for initialization of the text iterator.
  // If the iterator is not initialized, then the context menu does not show
  // spellcheck suggestions.
  // No migration is necessary, because the spellcheck language preference is
  // not user visible or modifiable in Chrome on Mac.
  if (dictionaries_pref.GetValue().empty() ||
      dictionaries_pref.GetValue().front() != platform_lang) {
    dictionaries_pref.SetValue(std::vector<std::string>(1, platform_lang));
  }
}

// InitMacDeferredSpellcheck orchestrates the startup sequence for Mac
// spellcheck when deferred initialization is enabled.
//
// The initialization flow works as follows:
// 1. Get the system's preferred spellchecker language off-thread (called via
//    ThreadPool::PostTaskAndReplyWithResult in the constructor). This is
//    done because querying the platform language can block the main thread.
// 2. Once the language is resolved, InitMacDeferredSpellcheck is run on the
//    UI thread.
// 3. Initialize Hunspell dictionaries first. This sets up the dictionary
//    infrastructure and StartRecordingMetrics() so metrics are not dropped.
// 4. Initialize platform language. If the preference changes, the preference
//    observer will trigger LoadDictionaries() to perform the actual load.
// 5. Load the custom dictionary database asynchronously.
void SpellcheckService::InitMacDeferredSpellcheck(
    const std::string& platform_lang) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // 1. Initialize Hunspell dictionaries first to set up metrics.
  InitializeDictionaries(base::DoNothing());

  // 2. Initialize platform language. If it updates the preference, the
  // observer will automatically call LoadDictionaries() again.
  InitializePlatformLanguageMacWithLanguage(platform_lang);

  // 3. Load custom dictionary database.
  custom_dictionary_->Load();
}
#endif

#if !BUILDFLAG(IS_MAC)
// static
void SpellcheckService::GetDictionaries(
    content::BrowserContext* browser_context,
    std::vector<Dictionary>* dictionaries) {
  PrefService* prefs = user_prefs::UserPrefs::Get(browser_context);
  absl::flat_hash_set<std::string> spellcheck_dictionaries;
  for (const auto& value :
       prefs->GetList(spellcheck::prefs::kSpellCheckDictionaries)) {
    const std::string* dictionary = value.GetIfString();
    if (dictionary)
      spellcheck_dictionaries.insert(*dictionary);
  }

  dictionaries->clear();
  std::vector<std::string> accept_languages =
      base::SplitString(prefs->GetString(language::prefs::kAcceptLanguages),
                        ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  for (const auto& accept_language : accept_languages) {
    Dictionary dictionary;
#if BUILDFLAG(IS_WIN)
    if (spellcheck::UseBrowserSpellChecker()) {
      SpellcheckService* spellcheck =
          SpellcheckServiceFactory::GetForContext(browser_context);
      if (spellcheck && spellcheck->UsesWindowsDictionary(accept_language))
        dictionary.language = accept_language;
    }

    if (dictionary.language.empty()) {
      dictionary.language =
          spellcheck::GetCorrespondingSpellCheckLanguage(accept_language);
    }
#else
    dictionary.language =
        spellcheck::GetCorrespondingSpellCheckLanguage(accept_language);
#endif  // BUILDFLAG(IS_WIN)

    if (dictionary.language.empty())
      continue;

    dictionary.used_for_spellcheck =
        spellcheck_dictionaries.contains(dictionary.language);
    dictionaries->push_back(dictionary);
  }
}
#endif  // !BUILDFLAG(IS_MAC)

// static
bool SpellcheckService::SignalStatusEvent(
    SpellcheckService::EventType status_type) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!g_status_event)
    return false;
  g_status_type = status_type;
  g_status_event->Signal();
  return true;
}

// static
std::string SpellcheckService::GetSupportedAcceptLanguageCode(
    const std::string& supported_language_full_tag,
    bool generic_only) {
  // Default to accept language in hardcoded list of Hunspell dictionaries
  // (kSupportedSpellCheckerLanguages).
  std::string supported_accept_language =
      spellcheck::GetCorrespondingSpellCheckLanguage(
          supported_language_full_tag);
  if (generic_only) {
    supported_accept_language = SpellcheckService::GetLanguageAndScriptTag(
        supported_accept_language,
        /* include_script_tag= */ false);
  }

#if BUILDFLAG(IS_WIN)
  if (!spellcheck::UseBrowserSpellChecker())
    return supported_accept_language;

  // Exclude dictionaries that are for private use, such as "ja-Latn-JP-x-ext".
  if (SpellcheckService::HasPrivateUseSubTag(supported_language_full_tag))
    return "";

  // Collect the hardcoded list of accept-languages supported by the browser,
  // that is, languages that can be added as preferred languages in the
  // languages settings page.
  std::vector<std::string> accept_languages;
  l10n_util::GetAcceptLanguages(&accept_languages);

  if (generic_only) {
    return GetSupportedAcceptLanguageCodeGenericOnly(
        supported_language_full_tag, accept_languages);
  }

  // First try exact match. Per BCP47, tags are in ASCII and should be treated
  // as case-insensitive (although there are conventions for the capitalization
  // of subtags).
  auto iter = std::ranges::find_if(
      accept_languages,
      [supported_language_full_tag](const auto& accept_language) {
        return base::EqualsCaseInsensitiveASCII(supported_language_full_tag,
                                                accept_language);
      });
  if (iter != accept_languages.end())
    return *iter;

  // Then try matching just the language and (optional) script subtags, but
  // not the region subtag. For example, Edge supports sr-Cyrl-RS as an accept
  // language, but not sr-Cyrl-CS. Matching language + script subtags assures
  // we get the correct script for spellchecking, and not use sr-Latn-RS if
  // language packs for both scripts are installed on the system.
  if (!supported_language_full_tag.contains("-")) {
    return "";
  }

  iter = std::ranges::find_if(
      accept_languages,
      [supported_language_full_tag](const auto& accept_language) {
        return base::EqualsCaseInsensitiveASCII(
            SpellcheckService::GetLanguageAndScriptTag(
                supported_language_full_tag,
                /* include_script_tag= */ true),
            SpellcheckService::GetLanguageAndScriptTag(
                accept_language,
                /* include_script_tag= */ true));
      });

  if (iter != accept_languages.end())
    return *iter;

  // Then try just matching the leading language subtag. E.g. Edge supports
  // kok as an accept language, but if the Konkani language pack is
  // installed the Windows spellcheck API reports kok-Deva-IN for the
  // dictionary name.
  return GetSupportedAcceptLanguageCodeGenericOnly(supported_language_full_tag,
                                                   accept_languages);

#else
  return supported_accept_language;
#endif  // BUILDFLAG(IS_WIN)
}

#if BUILDFLAG(IS_WIN)
// static
void SpellcheckService::EnableFirstUserLanguageForSpellcheck(
    PrefService* prefs) {
  // Ensure that spellcheck is enabled for the first language in the
  // accept languages list.
  base::ListValue user_dictionaries =
      prefs->GetList(spellcheck::prefs::kSpellCheckDictionaries).Clone();
  std::vector<std::string> user_languages =
      base::SplitString(prefs->GetString(language::prefs::kAcceptLanguages),
                        ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);

  // Some first run scenarios will add an accept language to preferences that
  // is not found in the hard-coded list in kAcceptLanguageList. Only
  // languages in kAcceptLanguageList can be spellchecked. An example is an
  // installation on a device where Finnish is the Windows display
  // language--the initial accept language preferences are observed to be
  // "fi-FI,fi,en-US,en". Only "fi" is contained in kAcceptLanguageList.
  std::string first_user_language;
  std::vector<std::string> accept_languages;
  l10n_util::GetAcceptLanguages(&accept_languages);
  for (const auto& user_language : user_languages) {
    if (std::ranges::contains(accept_languages, user_language)) {
      first_user_language = user_language;
      break;
    }
  }

  bool first_user_language_spellchecked = false;
  for (const auto& dictionary_value : user_dictionaries) {
    first_user_language_spellchecked =
        dictionary_value.GetString().contains(first_user_language);
    if (first_user_language_spellchecked)
      break;
  }

  if (!first_user_language_spellchecked) {
    user_dictionaries.Insert(user_dictionaries.begin(),
                             base::Value(first_user_language));
    prefs->SetList(spellcheck::prefs::kSpellCheckDictionaries,
                   std::move(user_dictionaries));
  }
}
#endif  // BUILDFLAG(IS_WIN)

void SpellcheckService::StartRecordingMetrics(bool spellcheck_enabled) {
  metrics_ = std::make_unique<SpellCheckHostMetrics>();
  auto record_configuration_metrics =
      RecordSpellingConfigurationMetrics(context_);
  if (record_configuration_metrics) {
    metrics_->RecordEnabledStats(spellcheck_enabled);
  }

  OnUseSpellingServiceChanged();

#if BUILDFLAG(IS_WIN)
  if (record_configuration_metrics) {
    RecordChromeLocalesStats();
    RecordSpellcheckLocalesStats();
  }
#endif  // BUILDFLAG(IS_WIN)
}

void SpellcheckService::InitForRenderer(content::RenderProcessHost* host) {
  base::ScopedUmaHistogramTimer timer("SpellCheck.Browser.InitForRendererTime");
  // Skip initialization of the spellcheck service for top chrome and NTP web UI
  // pages when Initial WebUI feature is enabled for optimizing browser startup.
  if (host->IsForTopChromeWebUI() &&
      base::FeatureList::IsEnabled(features::kInitialWebUI) &&
      features::kInitialWebUIWithoutSpellCheck.Get()) {
    return;
  }
  if (base::FeatureList::IsEnabled(
          features::kInitialWebUIWithoutSpellCheckForNtp) &&
      host->GetUserData(search::kIsNTPProcessKey)) {
    return;
  }

  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  content::BrowserContext* context = host->GetBrowserContext();
  if (SpellcheckServiceFactory::GetForContext(context) != this)
    return;

  const bool enable = IsSpellcheckEnabled();

  std::vector<spellcheck::mojom::SpellCheckBDictLanguagePtr> dictionaries;
  std::vector<std::string> custom_words;
  if (enable) {
    for (const auto& hunspell_dictionary : hunspell_dictionaries_) {
      dictionaries.push_back(spellcheck::mojom::SpellCheckBDictLanguage::New(
          hunspell_dictionary->GetDictionaryFile().Duplicate(),
          hunspell_dictionary->GetLanguage()));
    }

    std::set<std::string> custom_words_set = custom_dictionary_->GetWords();
    custom_words.assign(std::make_move_iterator(custom_words_set.begin()),
                        std::make_move_iterator(custom_words_set.end()));
  } else {
    // Disabling spell check should also disable spelling service.
    user_prefs::UserPrefs::Get(context)->SetBoolean(
        spellcheck::prefs::kSpellCheckUseSpellingService, false);
  }

  GetSpellCheckerForProcess(host)->Initialize(std::move(dictionaries),
                                              custom_words, enable);
}

SpellCheckHostMetrics* SpellcheckService::GetMetrics() const {
  return metrics_.get();
}

SpellcheckCustomDictionary* SpellcheckService::GetCustomDictionary() {
  return custom_dictionary_.get();
}

void SpellcheckService::LoadDictionaries() {
  hunspell_dictionaries_.clear();

  PrefService* prefs = user_prefs::UserPrefs::Get(context_);
  DCHECK(prefs);

  const base::ListValue& user_dictionaries =
      prefs->GetList(spellcheck::prefs::kSpellCheckDictionaries);
  const base::ListValue& forced_dictionaries =
      prefs->GetList(spellcheck::prefs::kSpellCheckForcedDictionaries);

  // Build a lookup of blocked dictionaries to skip loading them.
  const base::ListValue& blocked_dictionaries =
      prefs->GetList(spellcheck::prefs::kSpellCheckBlocklistedDictionaries);
  absl::flat_hash_set<std::string_view> blocked_dictionaries_lookup;
  blocked_dictionaries_lookup.reserve(blocked_dictionaries.size());
  for (const auto& blocked_dict : blocked_dictionaries) {
    blocked_dictionaries_lookup.insert(blocked_dict.GetString());
  }

  // Merge both lists of dictionaries. Use a set to avoid duplicates.
  std::set<std::string> dictionaries;
  for (const auto& dictionary_value : user_dictionaries) {
    if (!blocked_dictionaries_lookup.contains(dictionary_value.GetString())) {
      dictionaries.insert(dictionary_value.GetString());
    }
  }
  for (const auto& dictionary_value : forced_dictionaries) {
    dictionaries.insert(dictionary_value.GetString());
  }

  for (const std::string& dictionary : dictionaries) {
    // The spellcheck language passed to platform APIs may differ from the
    // accept language.
    std::string platform_spellcheck_language;
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    if (spellcheck::UseBrowserSpellChecker()) {
      std::string windows_dictionary_name =
          GetSupportedWindowsDictionaryLanguage(dictionary);
      if (!windows_dictionary_name.empty()) {
        platform_spellcheck_language =
            SpellcheckService::GetTagToPassToWindowsSpellchecker(
                dictionary, windows_dictionary_name);
      }
    }
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

    hunspell_dictionaries_.push_back(
        std::make_unique<SpellcheckHunspellDictionary>(
            dictionary, platform_spellcheck_language, context_, this));
    hunspell_dictionaries_.back()->AddObserver(this);
    hunspell_dictionaries_.back()->Load();
  }

#if BUILDFLAG(IS_WIN)
  if (RecordSpellingConfigurationMetrics(context_)) {
    RecordSpellcheckLocalesStats();
  }

#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  if (spellcheck::UseBrowserSpellChecker()) {
    // Only want to fire the callback on first call to LoadDictionaries
    // originating from InitializeDictionaries, since supported platform
    // dictionaries are cached throughout the browser session and not
    // dynamically updated. LoadDictionaries can be called multiple times in a
    // browser session, even before InitializeDictionaries is called, e.g. when
    // language settings are changed.
    if (!dictionaries_loaded() && dictionaries_loaded_callback_) {
      dictionaries_loaded_ = true;
      std::move(dictionaries_loaded_callback_).Run();
    }
    return;
  }
#endif  // BUILDFLAG(USE_BROWSER_SPELLCHECKER)
#endif  // BUILDFLAG(IS_WIN)
  dictionaries_loaded_ = true;
}

const std::vector<std::unique_ptr<SpellcheckHunspellDictionary>>&
SpellcheckService::GetHunspellDictionaries() {
  return hunspell_dictionaries_;
}

bool SpellcheckService::IsSpellcheckEnabled() const {
  const PrefService* prefs = user_prefs::UserPrefs::Get(context_);

  bool enable_if_uninitialized = false;
#if BUILDFLAG(IS_WIN)
  if (spellcheck::UseBrowserSpellChecker()) {
    // If initialization of the spellcheck service is on-demand, the
    // renderer-side SpellCheck object needs to start out as enabled in order
    // for a click on editable content to initialize the spellcheck service.
    if (!dictionaries_loaded())
      enable_if_uninitialized = true;
  }
#endif  // BUILDFLAG(IS_WIN)

  return prefs->GetBoolean(spellcheck::prefs::kSpellCheckEnable) &&
         (!hunspell_dictionaries_.empty() || enable_if_uninitialized);
}

void SpellcheckService::OnRenderProcessHostCreated(
    content::RenderProcessHost* host) {
  InitForRenderer(host);
}

void SpellcheckService::OnCustomDictionaryLoaded() {
  InitForAllRenderers();
}

void SpellcheckService::OnCustomDictionaryChanged(
    const SpellcheckCustomDictionary::Change& change) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  const auto additions = base::ToVector(change.to_add());
  const auto deletions = base::ToVector(change.to_remove());
  for (auto it = content::RenderProcessHost::AllHostsIterator(); !it.IsAtEnd();
       it.Advance()) {
    content::RenderProcessHost* process = it.GetCurrentValue();
    if (!process->IsInitializedAndNotDead() ||
        SpellcheckServiceFactory::GetForContext(process->GetBrowserContext()) !=
            this) {
      continue;
    }

    GetSpellCheckerForProcess(process)->CustomDictionaryChanged(additions,
                                                                deletions);
  }
}

void SpellcheckService::OnHunspellDictionaryInitialized(
    const std::string& language) {
  InitForAllRenderers();
}

void SpellcheckService::OnHunspellDictionaryDownloadBegin(
    const std::string& language) {
}

void SpellcheckService::OnHunspellDictionaryDownloadSuccess(
    const std::string& language) {
}

void SpellcheckService::OnHunspellDictionaryDownloadFailure(
    const std::string& language) {
}

void SpellcheckService::InitializeDictionaries(base::OnceClosure done) {
  // The dictionaries only need to be initialized once.
  if (dictionaries_loaded()) {
    std::move(done).Run();
    return;
  }

#if BUILDFLAG(IS_WIN)
  dictionaries_loaded_callback_ = std::move(done);
  // Need to initialize the platform spellchecker in order to record platform
  // locale stats even if the platform spellcheck feature is disabled.
  InitializePlatformSpellchecker();
#endif  // BUILDFLAG(IS_WIN)

  PrefService* prefs = user_prefs::UserPrefs::Get(context_);
  DCHECK(prefs);

  // Instantiates Metrics object for spellchecking to use.
  StartRecordingMetrics(
      prefs->GetBoolean(spellcheck::prefs::kSpellCheckEnable));

#if BUILDFLAG(IS_WIN)
  if (spellcheck::UseBrowserSpellChecker() && platform_spell_checker()) {
    spellcheck_platform::RetrieveSpellcheckLanguages(
        platform_spell_checker(),
        base::BindOnce(&SpellcheckService::InitWindowsDictionaryLanguages,
                       GetWeakPtr()));
    return;
  }
#endif  // BUILDFLAG(IS_WIN)

  // Using Hunspell.
  LoadDictionaries();
}

#if BUILDFLAG(IS_WIN)
void SpellcheckService::InitWindowsDictionaryLanguages(
    const std::vector<std::string>& windows_spellcheck_languages) {
  windows_spellcheck_dictionary_map_.clear();
  for (const auto& windows_spellcheck_language : windows_spellcheck_languages) {
    std::string accept_language =
        SpellcheckService::GetSupportedAcceptLanguageCode(
            windows_spellcheck_language, /* generic_only */ false);
    AddWindowsSpellcheckDictionary(accept_language,
                                   windows_spellcheck_language);

    // There is one unfortunate special case (so far the only one known). The
    // accept language "sr" is supported, and if you use it as a display
    // language you see Cyrillic script. If a Windows language pack is
    // installed that supports "sr-Cyrl-*", mark the "sr" accept language
    // as having Windows spellcheck support instead of using Hunspell.
    if (base::EqualsCaseInsensitiveASCII(
            "sr-Cyrl", SpellcheckService::GetLanguageAndScriptTag(
                           windows_spellcheck_language,
                           /* include_script_tag= */ true))) {
      AddWindowsSpellcheckDictionary("sr", windows_spellcheck_language);
    }

    // Add the generic language with the region subtag removed too if it exists
    // in the list of accept languages, and use it when calling the Windows
    // spellcheck APIs. For example, if the preferred language settings include
    // just generic Portuguese (pt), but the Portuguese (Brazil) platform
    // language pack (pt-BR) is installed, we want an entry for it so that the
    // generic Portuguese language can be enabled for spellchecking. The Windows
    // platform spellcheck API has logic to load the pt-BR dictionary if only pt
    // is specified as the BCP47 language tag. The use of a map in
    // AddWindowsSpellcheckDictionary ensures there won't be duplicate entries
    // if a generic language was already added above (ar-SA would already be
    // mapped to ar since the accept language ar-SA is not recognized by the
    // browser e.g.).
    accept_language = SpellcheckService::GetSupportedAcceptLanguageCode(
        windows_spellcheck_language, /* generic_only */ true);
    AddWindowsSpellcheckDictionary(accept_language, accept_language);
  }

  // A user may have removed a language pack for a non-Hunspell language after
  // enabling it for spellcheck on the language settings page. Remove
  // preferences for this language so that there is no attempt to load a
  // non-existent Hunspell dictionary, and so that Hunspell spellchecking isn't
  // broken because of the failed load. This also handles the case where the
  // primary preferred language is enabled for spellchecking during first run,
  // but it's now determined that there is neither Windows platform nor Hunspell
  // dictionary support for that language.
  PrefService* prefs = user_prefs::UserPrefs::Get(context_);
  DCHECK(prefs);
  // When following object goes out of scope, preference change observers will
  // be notified (even if there is no preference change).
  ScopedListPrefUpdate update(prefs,
                              spellcheck::prefs::kSpellCheckDictionaries);
  update->EraseIf([this](const base::Value& entry) {
    const std::string dictionary_name = entry.GetString();
    return (!UsesWindowsDictionary(dictionary_name) &&
            spellcheck::GetCorrespondingSpellCheckLanguage(dictionary_name)
                .empty());
  });

  // No need to call LoadDictionaries() as when the ScopedListPrefUpdate object
  // goes out of scope, the preference change handler will do this.
}

bool SpellcheckService::UsesWindowsDictionary(
    std::string accept_language) const {
  return !GetSupportedWindowsDictionaryLanguage(accept_language).empty();
}
#endif  // BUILDFLAG(IS_WIN)

// static
void SpellcheckService::OverrideBinderForTesting(SpellCheckerBinder binder) {
  GetSpellCheckerBinderOverride() = std::move(binder);
}

// static
std::string SpellcheckService::GetLanguageAndScriptTag(
    const std::string& full_tag,
    bool include_script_tag) {
  if (full_tag.empty())
    return "";

  std::string language_and_script_tag;

  std::vector<std::string> subtags = base::SplitString(
      full_tag, "-", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  // Language subtag is required, all others optional.
  DCHECK_GE(subtags.size(), 1ULL);
  std::vector<std::string> subtag_tokens_to_pass;
  subtag_tokens_to_pass.push_back(subtags.front());
  subtags.erase(subtags.begin());

  // The optional script subtag always follows the language subtag, and is 4
  // characters in length.
  if (include_script_tag) {
    if (!subtags.empty() && subtags.front().length() == 4) {
      subtag_tokens_to_pass.push_back(subtags.front());
    }
  }

  language_and_script_tag = base::JoinString(subtag_tokens_to_pass, "-");

  return language_and_script_tag;
}

#if BUILDFLAG(IS_WIN)
// static
std::string SpellcheckService::GetSupportedAcceptLanguageCodeGenericOnly(
    const std::string& supported_language_full_tag,
    const std::vector<std::string>& accept_languages) {
  auto iter = std::ranges::find_if(
      accept_languages,
      [supported_language_full_tag](const auto& accept_language) {
        return base::EqualsCaseInsensitiveASCII(
            SpellcheckService::GetLanguageAndScriptTag(
                supported_language_full_tag,
                /* include_script_tag= */ false),
            SpellcheckService::GetLanguageAndScriptTag(
                accept_language,
                /* include_script_tag= */ false));
      });

  if (iter != accept_languages.end()) {
    // Special case for Serbian--"sr" implies Cyrillic script. Don't mark it as
    // supported for sr-Latn*.
    if (base::EqualsCaseInsensitiveASCII(
            SpellcheckService::GetLanguageAndScriptTag(
                supported_language_full_tag,
                /* include_script_tag= */ true),
            "sr-Latn")) {
      return "";
    }
    return *iter;
  }

  return "";
}

// static
bool SpellcheckService::HasPrivateUseSubTag(const std::string& full_tag) {
  std::vector<std::string> subtags = base::SplitString(
      full_tag, "-", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);

  // Private use subtags are separated from the other subtags by the reserved
  // single-character subtag 'x'.
  return std::ranges::contains(subtags, "x");
}

// static
std::string SpellcheckService::GetTagToPassToWindowsSpellchecker(
    const std::string& accept_language,
    const std::string& supported_language_full_tag) {
  // First try exact match. Per BCP47, tags are in ASCII and should be treated
  // as case-insensitive (although there are conventions for the capitalization
  // of subtags, they are sometimes broken).
  if (base::EqualsCaseInsensitiveASCII(supported_language_full_tag,
                                       accept_language)) {
    // Unambiguous spellcheck dictionary to be used.
    return supported_language_full_tag;
  }

  // Accept language does not match script or region subtags.
  // If there is a script subtag, include it, to avoid for example passing
  // "sr" which is ambiguous as Serbian can use Cyrillic or Latin script.
  // There is one unfortunate special case (so far the only one known). The
  // accept language "sr" is supported, and if you use it as a display
  // language you see Cyrillic script. However, the Windows spellcheck API
  // returns "sr-Latn-*" dictionaries if the unqualified language tag is
  // passed. The following forces Windows spellchecking to use Cyrillic script
  // in this case, and if the language pack is not installed there will be a
  // fallback to Hunspell support when spellchecking is performed.
  if (base::EqualsCaseInsensitiveASCII("sr", accept_language))
    return "sr-Cyrl";

  return SpellcheckService::GetLanguageAndScriptTag(
      supported_language_full_tag,
      /* include_script_tag= */ true);
}

#endif  // BUILDFLAG(IS_WIN)

// static
void SpellcheckService::AttachStatusEvent(base::WaitableEvent* status_event) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  g_status_event = status_event;
}

// static
SpellcheckService::EventType SpellcheckService::GetStatusEvent() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  return g_status_type;
}

mojo::Remote<spellcheck::mojom::SpellChecker>
SpellcheckService::GetSpellCheckerForProcess(content::RenderProcessHost* host) {
  mojo::Remote<spellcheck::mojom::SpellChecker> spellchecker;
  auto receiver = spellchecker.BindNewPipeAndPassReceiver();
  auto binder = GetSpellCheckerBinderOverride();
  if (binder)
    binder.Run(std::move(receiver));
  else
    host->BindReceiver(std::move(receiver));
  return spellchecker;
}

void SpellcheckService::InitForAllRenderers() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  for (auto it = content::RenderProcessHost::AllHostsIterator(); !it.IsAtEnd();
       it.Advance()) {
    content::RenderProcessHost* process = it.GetCurrentValue();
    // Do not test GetProcess().Handle() here: a renderer that has been
    // initialized but whose process is still launching has no handle yet, and
    // skipping it drops the notification for good. Its mojo channel already
    // exists, so messages queue up and are delivered once the process is up.
    // This matches OnCustomDictionaryChanged() below.
    if (process && process->IsInitializedAndNotDead()) {
      InitForRenderer(process);
    }
  }
}

void SpellcheckService::OnSpellCheckDictionariesChanged() {
  // If there are hunspell dictionaries, then fire off notifications to the
  // renderers after the dictionaries are finished loading.
  LoadDictionaries();

  // If there are no hunspell dictionaries to load, then immediately let the
  // renderers know the new state.
  if (hunspell_dictionaries_.empty()) {
#if !BUILDFLAG(IS_MAC)
    // Only update non-MacOS platform because basic spell check on Mac OS
    // is controlled by OS and doesn't depend on users' dictionaries pref
    user_prefs::UserPrefs::Get(context_)->SetBoolean(
        spellcheck::prefs::kSpellCheckEnable, false);
#endif  // !BUILDFLAG(IS_MAC)
    InitForAllRenderers();
  }
}

void SpellcheckService::OnUseSpellingServiceChanged() {
  bool enabled = pref_change_registrar_.prefs()->GetBoolean(
      spellcheck::prefs::kSpellCheckUseSpellingService);
  if (metrics_ && RecordSpellingConfigurationMetrics(context_)) {
    metrics_->RecordSpellingServiceStats(enabled);
  }
}

void SpellcheckService::OnAcceptLanguagesChanged() {
  // Accept-Languages and spell check are decoupled on CrOS.
#if !BUILDFLAG(IS_CHROMEOS)
  std::vector<std::string> accept_languages = GetNormalizedAcceptLanguages();

  StringListPrefMember dictionaries_pref;
  dictionaries_pref.Init(spellcheck::prefs::kSpellCheckDictionaries,
                         user_prefs::UserPrefs::Get(context_));
  std::vector<std::string> dictionaries = dictionaries_pref.GetValue();
  std::vector<std::string> filtered_dictionaries;

  for (const auto& dictionary : dictionaries) {
    if (std::ranges::contains(accept_languages, dictionary)) {
      filtered_dictionaries.push_back(dictionary);
    }
  }

  dictionaries_pref.SetValue(filtered_dictionaries);

#if BUILDFLAG(IS_WIN)
  if (RecordSpellingConfigurationMetrics(context_)) {
    RecordChromeLocalesStats();
  }
#endif  // BUILDFLAG(IS_WIN)
#endif  // !BUILDFLAG(IS_CHROMEOS)
}

std::vector<std::string> SpellcheckService::GetNormalizedAcceptLanguages(
    bool normalize_for_spellcheck) const {
  PrefService* prefs = user_prefs::UserPrefs::Get(context_);
  std::vector<std::string> accept_languages =
      base::SplitString(prefs->GetString(language::prefs::kAcceptLanguages),
                        ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);

  if (normalize_for_spellcheck) {
    std::ranges::transform(
        accept_languages, accept_languages.begin(),
        [&](const std::string& language) {
#if BUILDFLAG(IS_WIN)
          if (spellcheck::UseBrowserSpellChecker() &&
              UsesWindowsDictionary(language))
            return language;
#endif  // BUILDFLAG(IS_WIN)
          return spellcheck::GetCorrespondingSpellCheckLanguage(language);
        });
  }

  return accept_languages;
}

#if BUILDFLAG(IS_WIN)
void SpellcheckService::InitializePlatformSpellchecker() {
  // The Windows spell checker must be created before the dictionaries are
  // initialized. Note it is instantiated even if only Hunspell is being used
  // since metrics on the availability of Windows platform language packs are
  // being recorded. Thus method should only be called once, except in test
  // code.
  if (!platform_spell_checker()) {
    platform_spell_checker_ = std::make_unique<WindowsSpellChecker>(
        base::ThreadPool::CreateCOMSTATaskRunner({base::MayBlock()}));
  }
}

void SpellcheckService::RecordSpellcheckLocalesStats() {
  if (metrics_ && platform_spell_checker() && !hunspell_dictionaries_.empty()) {
    std::vector<std::string> hunspell_locales;
    for (auto& dict : hunspell_dictionaries_) {
      hunspell_locales.push_back(dict->GetLanguage());
    }
    spellcheck_platform::RecordSpellcheckLocalesStats(
        platform_spell_checker(), std::move(hunspell_locales));
  }
}

void SpellcheckService::RecordChromeLocalesStats() {
  if (metrics_ && platform_spell_checker()) {
    std::vector<std::string> accept_languages =
        GetNormalizedAcceptLanguages(/* normalize_for_spellcheck */ false);
    if (!accept_languages.empty()) {
      spellcheck_platform::RecordChromeLocalesStats(
          platform_spell_checker(), std::move(accept_languages));
    }
  }
}

void SpellcheckService::AddWindowsSpellcheckDictionary(
    const std::string& accept_language,
    const std::string& supported_language_full_tag) {
  if (!accept_language.empty()) {
    windows_spellcheck_dictionary_map_.insert(
        {accept_language, supported_language_full_tag});
  }
}

std::string SpellcheckService::GetSupportedWindowsDictionaryLanguage(
    const std::string& accept_language) const {
  // BCP47 language tag used by the Windows spellchecker API.
  std::string spellcheck_language;

  auto it = windows_spellcheck_dictionary_map_.find(accept_language);
  if (it != windows_spellcheck_dictionary_map_.end())
    spellcheck_language = it->second;

  return spellcheck_language;
}

void SpellcheckService::AddSpellcheckLanguagesForTesting(
    const std::vector<std::string>& languages) {
  InitializePlatformSpellchecker();
  if (platform_spell_checker()) {
    spellcheck_platform::AddSpellcheckLanguagesForTesting(
        platform_spell_checker(), languages);
  }
}
#endif  // BUILDFLAG(IS_WIN)
