// 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 "components/spellcheck/renderer/spellcheck_provider.h"

#include <algorithm>
#include <unordered_map>
#include <vector>

#include "base/compiler_specific.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.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/common/spellcheck_result.h"
#include "components/spellcheck/common/spelling_marker.h"
#include "components/spellcheck/renderer/spellcheck.h"
#include "components/spellcheck/renderer/spellcheck_language.h"
#include "components/spellcheck/renderer/spellcheck_renderer_metrics.h"
#include "components/spellcheck/spellcheck_buildflags.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_thread.h"
#include "services/service_manager/public/cpp/local_interface_provider.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/public/web/web_console_message.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_text_checking_completion.h"
#include "third_party/blink/public/web/web_text_checking_result.h"
#include "third_party/blink/public/web/web_text_decoration_type.h"

using blink::WebElement;
using blink::WebLocalFrame;
using blink::WebString;
using blink::WebTextCheckingCompletion;
using blink::WebTextCheckingResult;
using blink::WebTextDecorationType;

static_assert(static_cast<int>(blink::kWebTextDecorationTypeSpelling) ==
                  static_cast<int>(spellcheck::Decoration::SPELLING),
              "mismatching enums");
static_assert(static_cast<int>(blink::kWebTextDecorationTypeGrammar) ==
                  static_cast<int>(spellcheck::Decoration::GRAMMAR),
              "mismatching enums");

namespace {

spellcheck::Decoration MapToDecoration(
    blink::WebSpellingMarker::SpellingMarkerType marker_type) {
  switch (marker_type) {
    case blink::WebSpellingMarker::SpellingMarkerType::kSpelling:
      return spellcheck::Decoration::SPELLING;
    case blink::WebSpellingMarker::SpellingMarkerType::kGrammar:
      return spellcheck::Decoration::GRAMMAR;
  }
}

spellcheck::SpellingMarker MapToSpellingMarker(
    const blink::WebSpellingMarker& marker) {
  return spellcheck::SpellingMarker(marker.start, marker.end,
                                    MapToDecoration(marker.marker_type));
}

}  // namespace

class SpellCheckProvider::DictionaryUpdateObserverImpl
    : public DictionaryUpdateObserver {
 public:
  explicit DictionaryUpdateObserverImpl(SpellCheckProvider* owner);
  ~DictionaryUpdateObserverImpl() override;

  // DictionaryUpdateObserver:
  void OnDictionaryUpdated(const std::vector<WebString>& words_added) override;

 private:
  raw_ptr<SpellCheckProvider> owner_;
};

SpellCheckProvider::DictionaryUpdateObserverImpl::DictionaryUpdateObserverImpl(
    SpellCheckProvider* owner)
    : owner_(owner) {
  owner_->spellcheck_->AddDictionaryUpdateObserver(this);
}

SpellCheckProvider::DictionaryUpdateObserverImpl::
    ~DictionaryUpdateObserverImpl() {
  owner_->spellcheck_->RemoveDictionaryUpdateObserver(this);
}

void SpellCheckProvider::DictionaryUpdateObserverImpl::OnDictionaryUpdated(
    const std::vector<WebString>& words_added) {
  // Clear only cache. Current pending requests should continue as they are.
  owner_->last_request_.clear();
  owner_->last_results_.clear();

  // owner_->render_frame() is nullptr in unit tests.
  if (auto* render_frame = owner_->render_frame()) {
    DCHECK(render_frame->GetWebFrame());
    render_frame->GetWebFrame()->RemoveSpellingMarkersUnderWords(words_added);
  }
}

SpellCheckProvider::SpellCheckProvider(content::RenderFrame* render_frame,
                                       SpellCheck* spellcheck)
    : content::RenderFrameObserver(render_frame), spellcheck_(spellcheck) {
  DCHECK(spellcheck_);
  if (render_frame)  // NULL in unit tests.
    render_frame->GetWebFrame()->SetTextCheckClient(this);

  dictionary_update_observer_ =
      std::make_unique<DictionaryUpdateObserverImpl>(this);
}

SpellCheckProvider::~SpellCheckProvider() {
}

void SpellCheckProvider::ResetDictionaryUpdateObserverForTesting() {
  dictionary_update_observer_.reset();
}

void SpellCheckProvider::RequestTextChecking(
    const std::u16string& text,
    const std::vector<spellcheck::SpellingMarker>& spelling_markers,
    blink::WebTextCheckClient::ShouldForceRefreshTextCheckService
        should_force_refresh,
    std::unique_ptr<WebTextCheckingCompletion> completion) {
  // Ignore invalid requests.
  if (text.empty() || !HasWordCharacters(text, 0)) {
    completion->DidCancelCheckingText();
    return;
  }

  // Try to satisfy check from cache.
  if (should_force_refresh ==
          blink::WebTextCheckClient::ShouldForceRefreshTextCheckService::kNo &&
      SatisfyRequestFromCache(text, completion.get())) {
    return;
  }

  // Send this text to a browser. A browser checks the user profile and send
  // this text to the Spelling service only if a user enables this feature.
  last_request_.clear();
  last_results_.clear();
  last_identifier_ = text_check_completions_.Add(std::move(completion));

#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  if (spellcheck::UseBrowserSpellChecker()) {
#if BUILDFLAG(IS_WIN)
    if (!dictionaries_loaded_) {
      // Initialize the spellcheck service on demand (this spellcheck request
      // could be the result of the first click in editable content), then
      // complete the text check request when the dictionaries are loaded.
      // The delayed spell check service initialization sequence, starting from
      // when the user clicks in editable content, is as follows:
      // - SpellcheckProvider::RequestTextChecking (Renderer, this method)
      // - SpellCheckHostChromeImpl::InitializeDictionaries (Browser)
      // - SpellcheckService::InitializeDictionaries (Browser)
      // - SpellCheckHostChromeImpl::OnDictionariesInitialized (Browser)
      // - SpellcheckProvider::OnRespondInitializeDictionaries (Renderer)
      GetSpellCheckHost().InitializeDictionaries(
          base::BindOnce(&SpellCheckProvider::OnRespondInitializeDictionaries,
                         weak_factory_.GetWeakPtr(), text));
      return;
    }
#endif  // BUILDFLAG(IS_WIN)

    RequestTextCheckingFromBrowser(text, spelling_markers);
  }
#endif  // BUILDFLAG(USE_BROWSER_SPELLCHECKER)

#if BUILDFLAG(USE_RENDERER_SPELLCHECKER)
  if (!spellcheck::UseBrowserSpellChecker()) {
    GetSpellCheckHost().CallSpellingService(
        text,
        base::BindOnce(&SpellCheckProvider::OnRespondSpellingService,
                       weak_factory_.GetWeakPtr(), last_identifier_, text));
  }
#endif  // BUILDFLAG(USE_RENDERER_SPELLCHECKER)
}

#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
void SpellCheckProvider::RequestTextCheckingFromBrowser(
    const std::u16string& text,
    const std::vector<spellcheck::SpellingMarker>& spelling_markers) {
  DCHECK(spellcheck::UseBrowserSpellChecker());
#if BUILDFLAG(IS_WIN)

  // Determine whether a hybrid check is needed.
  bool use_hunspell = spellcheck_->EnabledLanguageCount() > 0;
  bool use_native =
      spellcheck_->EnabledLanguageCount() != spellcheck_->LanguageCount();

  if (!use_hunspell && !use_native) {
    OnRespondTextCheck(last_identifier_, text, /*results=*/{});
    return;
  }

  if (!use_native) {
    // No language can be handled by the native spell checker. Use the regular
    // Hunspell code path.
    GetSpellCheckHost().CallSpellingService(
        text,
        base::BindOnce(&SpellCheckProvider::OnRespondSpellingService,
                       weak_factory_.GetWeakPtr(), last_identifier_, text));
    return;
  }

  // Some languages can be handled by the native spell checker. Use the
  // regular browser spell check code path. If hybrid spell check is
  // required (i.e. some locales must be checked by Hunspell), misspellings
  // from the native spell checker will be double-checked with Hunspell in
  // the |OnRespondTextCheck| callback.
  hybrid_requests_info_[last_identifier_] = {/*used_hunspell=*/use_hunspell,
                                             /*used_native=*/use_native,
                                             base::TimeTicks::Now()};
#endif  // BUILDFLAG(IS_WIN)

  // Text check (unified request for grammar and spell check) is only
  // available for browser process, so we ask the system spellchecker
  // over mojo or return an empty result if the checker is not available.
  GetSpellCheckHost().RequestTextCheck(
      text, spelling_markers,
      base::BindOnce(&SpellCheckProvider::OnRespondTextCheck,
                     weak_factory_.GetWeakPtr(), last_identifier_, text));
}

#if BUILDFLAG(IS_WIN)
void SpellCheckProvider::OnRespondInitializeDictionaries(
    const std::u16string& text,
    std::vector<spellcheck::mojom::SpellCheckBDictLanguagePtr> dictionaries,
    const std::vector<std::string>& custom_words,
    bool enable) {
  DCHECK(!dictionaries_loaded_);
  dictionaries_loaded_ = true;

  // Because the SpellChecker and SpellCheckHost mojo interfaces use different
  // channels, there is no guarantee that the SpellChecker::Initialize response
  // will be received before the SpellCheckHost::InitializeDictionaries
  // callback. If the order is reversed, no spellcheck will be performed since
  // the renderer side thinks there are no dictionaries available. Ensure that
  // the SpellChecker is initialized before performing a spellcheck.
  spellcheck_->Initialize(std::move(dictionaries), custom_words, enable);

  RequestTextCheckingFromBrowser(text, /*spelling_markers=*/{});
}
#endif  // BUILDFLAG(IS_WIN)
#endif  // BUILDFLAG(USE_BROWSER_SPELLCHECKER)

void SpellCheckProvider::DidCreateNewDocument() {
  // SpellCheckCustomDictionary is document-scoped: a fresh document starts
  // with no custom words. Record the outgoing document's word count for
  // documents that actually used the API, so the per-document cap can be
  // revisited with real-world data.
  if (!document_custom_words_.empty()) {
    UMA_HISTOGRAM_COUNTS_100000(
        "Spellcheck.DocumentCustomDictionary.WordCount",
        static_cast<int>(document_custom_words_.size()));
  }
  document_custom_words_.clear();
  document_custom_dictionary_overflow_warned_ = false;
}

void SpellCheckProvider::FocusedElementChanged(
    const blink::WebElement& unused) {
#if BUILDFLAG(IS_ANDROID)
  if (!spell_check_host_.is_bound())
    return;

  WebLocalFrame* frame = render_frame()->GetWebFrame();
  WebElement element = frame->GetDocument().IsNull()
                           ? WebElement()
                           : frame->GetDocument().FocusedElement();
  bool enabled = !element.IsNull() && element.IsEditable();
  if (!enabled)
    GetSpellCheckHost().DisconnectSessionBridge();
#endif  // BUILDFLAG(IS_ANDROID)
}

spellcheck::mojom::SpellCheckHost& SpellCheckProvider::GetSpellCheckHost() {
  if (spell_check_host_) {
    return *spell_check_host_.get();
  }

  // We shodulnt't get here in tests, `spell_check_host_` should have been set.
  CHECK(render_frame());

  render_frame()->GetBrowserInterfaceBroker().GetInterface(
      spell_check_host_.BindNewPipeAndPassReceiver());
  return *spell_check_host_.get();
}

bool SpellCheckProvider::IsSpellCheckingEnabled() const {
  return spellcheck_->IsSpellcheckEnabled();
}

void SpellCheckProvider::CheckSpelling(
    const WebString& text,
    size_t& offset,
    size_t& length,
    std::vector<blink::WebString>* optional_suggestions) {
  std::u16string word = text.Utf16();

  if (optional_suggestions) {
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    base::TimeTicks suggestions_start = base::TimeTicks::Now();
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    // Retrieve suggestions from Hunspell. Windows platform spellchecker
    // suggestions are retrieved in SpellingMenuObserver::InitMenu on the
    // browser process side to avoid a blocking IPC.
    spellcheck::PerLanguageSuggestions per_language_suggestions;
    spellcheck_->SpellCheckWord(word, GetSpellCheckHost(), &offset, &length,
                                &per_language_suggestions);
    ApplyDocumentCustomWords(word, offset, length);
    if (length == 0) {
      per_language_suggestions.clear();
    }

#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
    spellcheck_renderer_metrics::RecordHunspellSuggestionDuration(
        base::TimeTicks::Now() - suggestions_start);
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

    std::vector<std::u16string> suggestions;
    spellcheck::FillSuggestions(per_language_suggestions, &suggestions);
    *optional_suggestions = base::ToVector(suggestions, &WebString::FromUtf16);
    spellcheck_renderer_metrics::RecordCheckedTextLengthWithSuggestions(
        base::saturated_cast<int>(word.size()));
  } else {
    spellcheck_->SpellCheckWord(word, GetSpellCheckHost(), &offset, &length,
                                /* optional suggestions vector */ nullptr);
    ApplyDocumentCustomWords(word, offset, length);
    spellcheck_renderer_metrics::RecordCheckedTextLengthNoSuggestions(
        base::saturated_cast<int>(word.size()));

    // If optional_suggestions is not requested, the API is called
    // for marking. So we use this for counting markable words.
    GetSpellCheckHost().NotifyChecked(word, 0 < length);
  }
}

void SpellCheckProvider::RequestCheckingOfText(
    const WebString& text,
    const std::vector<blink::WebSpellingMarker>& spelling_markers,
    blink::WebTextCheckClient::ShouldForceRefreshTextCheckService
        should_force_refresh,
    std::unique_ptr<WebTextCheckingCompletion> completion) {
  RequestTextChecking(text.Utf16(),
                      base::ToVector(spelling_markers, &MapToSpellingMarker),
                      should_force_refresh, std::move(completion));
  spellcheck_renderer_metrics::RecordAsyncCheckedTextLength(
      base::saturated_cast<int>(text.length()));
}

void SpellCheckProvider::ApplyDocumentCustomWords(const std::u16string& word,
                                                  size_t& offset,
                                                  size_t& length) const {
  // An optimization to avoid a substring allocation and set lookup
  // on this per-word path; behavior is unchanged if it is removed.
  if (length == 0 || document_custom_words_.empty()) {
    return;
  }
  if (document_custom_words_.contains(word.substr(offset, length))) {
    offset = 0;
    length = 0;
  }
}

void SpellCheckProvider::SpellCheckCustomDictionaryChanged(
    const std::vector<std::string>& words_added,
    const std::vector<std::string>& words_removed) {
  // Enforce the per-document word-count cap and per-word length limit at the
  // API entry point. document_custom_words_ is the live set, so the cap bounds
  // the resident set rather than lifetime churn.
  for (const std::string& word : words_removed) {
    document_custom_words_.erase(base::UTF8ToUTF16(word));
  }

  std::vector<std::string> effective_added;
  effective_added.reserve(words_added.size());
  bool dropped_some = false;
  for (const std::string& word : words_added) {
    if (word.size() > spellcheck::kMaxDocumentCustomDictionaryWordBytes) {
      dropped_some = true;
      continue;
    }
    if (document_custom_words_.size() >=
        spellcheck::kMaxDocumentCustomDictionaryWords) {
      // Once the cap is reached every later word would be rejected.
      dropped_some = true;
      break;
    }
    // Below the cap: insert once and forward only words that were actually new.
    if (document_custom_words_.insert(base::UTF8ToUTF16(word)).second) {
      effective_added.push_back(word);
    }
  }

  if (dropped_some && !document_custom_dictionary_overflow_warned_) {
    if (auto* frame = render_frame()) {
      frame->GetWebFrame()->AddMessageToConsole(blink::WebConsoleMessage(
          blink::mojom::ConsoleMessageLevel::kWarning,
          blink::WebString::FromAscii(
              "SpellCheckCustomDictionary: per-document word limit reached "
              "or a word exceeded the maximum length; some additions were "
              "ignored.")));
    }
    document_custom_dictionary_overflow_warned_ = true;
  }

  spellcheck_->SpellCheckCustomDictionaryChanged(effective_added,
                                                 words_removed);
}

#if BUILDFLAG(USE_RENDERER_SPELLCHECKER)
void SpellCheckProvider::OnRespondSpellingService(
    int identifier,
    const std::u16string& line,
    bool success,
    const std::vector<SpellCheckResult>& results) {
  if (!text_check_completions_.Lookup(identifier))
    return;
  std::unique_ptr<WebTextCheckingCompletion> completion(
      text_check_completions_.Replace(identifier, nullptr));
  text_check_completions_.Remove(identifier);

  // If |success| is false, we use local spellcheck as a fallback.
  if (!success) {
    spellcheck_->RequestTextChecking(line, std::move(completion),
                                     weak_factory_.GetWeakPtr());
    return;
  }

  // Double-check the returned spellchecking results with Hunspell to visualize
  // the differences between ours and the enhanced spell checker.
  std::vector<blink::WebTextCheckingResult> textcheck_results;
  spellcheck_->CreateTextCheckingResults(
      SpellCheck::USE_HUNSPELL_FOR_GRAMMAR, GetSpellCheckHost(),
      /*line_offset=*/0, line, results, &textcheck_results,
      document_custom_words_.empty() ? nullptr : &document_custom_words_);
  completion->DidFinishCheckingText(textcheck_results);

  // Cache the request and the converted results.
  last_request_ = line;
  last_results_.swap(textcheck_results);
}
#endif

bool SpellCheckProvider::HasWordCharacters(const std::u16string& text,
                                           size_t index) const {
  const char16_t* data = text.data();
  size_t length = text.length();
  while (index < length) {
    uint32_t code = 0;
    UNSAFE_TODO(U16_NEXT(data, index, length, code));
    UErrorCode error = U_ZERO_ERROR;
    if (uscript_getScript(code, &error) != USCRIPT_COMMON)
      return true;
  }
  return false;
}

#if BUILDFLAG(USE_BROWSER_SPELLCHECKER)
void SpellCheckProvider::OnRespondTextCheck(
    int identifier,
    const std::u16string& line,
    const std::vector<SpellCheckResult>& results) {
  DCHECK(spellcheck_);
  if (!text_check_completions_.Lookup(identifier))
    return;
  std::unique_ptr<WebTextCheckingCompletion> completion(
      text_check_completions_.Replace(identifier, nullptr));
  text_check_completions_.Remove(identifier);
  std::vector<blink::WebTextCheckingResult> textcheck_results;

  SpellCheck::ResultFilter result_filter = SpellCheck::DO_NOT_MODIFY;

#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  const auto& request_info = hybrid_requests_info_.find(identifier);
  if (spellcheck::UseBrowserSpellChecker() &&
      request_info != hybrid_requests_info_.end() &&
      request_info->second.used_hunspell && request_info->second.used_native) {
    // Not all locales could be checked by the native spell checker. Verify each
    // mistake against Hunspell in the locales that weren't checked.
    result_filter = SpellCheck::USE_HUNSPELL_FOR_HYBRID_CHECK;
  }
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

  spellcheck_->CreateTextCheckingResults(
      result_filter, GetSpellCheckHost(),
      /*line_offset=*/0, line, results, &textcheck_results,
      document_custom_words_.empty() ? nullptr : &document_custom_words_);

#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
  if (request_info != hybrid_requests_info_.end()) {
    spellcheck_renderer_metrics::RecordSpellcheckDuration(
        base::TimeTicks::Now() - request_info->second.request_start_ticks,
        request_info->second.used_hunspell, request_info->second.used_native);
    hybrid_requests_info_.erase(request_info);
  }
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)

  completion->DidFinishCheckingText(textcheck_results);

  // Cache the request and the converted results.
  last_request_ = line;
  last_results_.swap(textcheck_results);
}
#endif  // BUILDFLAG(USE_BROWSER_SPELLCHECKER)

bool SpellCheckProvider::SatisfyRequestFromCache(
    const std::u16string& text,
    WebTextCheckingCompletion* completion) {
  size_t last_length = last_request_.length();
  if (!last_length)
    return false;

  // Send back the |last_results_| if the |last_request_| is a substring of
  // |text| and |text| does not have more words to check. Provider cannot cancel
  // the spellcheck request here, because WebKit might have discarded the
  // previous spellcheck results and erased the spelling markers in response to
  // the user editing the text.
  std::u16string request(text);
  size_t text_length = request.length();
  if (text_length >= last_length &&
      !request.compare(0, last_length, last_request_)) {
    if (text_length == last_length || !HasWordCharacters(text, last_length)) {
      completion->DidFinishCheckingText(last_results_);
      return true;
    }
  }

  // Create a subset of the cached results and return it if the given text is a
  // substring of the cached text.
  if (text_length < last_length &&
      !last_request_.compare(0, text_length, request)) {
    size_t result_size = 0;
    for (size_t i = 0; i < last_results_.size(); ++i) {
      size_t start = last_results_[i].location;
      size_t end = start + last_results_[i].length;
      if (start <= text_length && end <= text_length)
        ++result_size;
    }
    std::vector<blink::WebTextCheckingResult> results =
        base::ToVector(base::span(last_results_).first(result_size));
    completion->DidFinishCheckingText(results);
    return true;
  }

  return false;
}

void SpellCheckProvider::OnDestruct() {
  // Capture the final word count for documents that used the API but were
  // torn down without a follow-on navigation (e.g., the frame was removed).
  if (!document_custom_words_.empty()) {
    UMA_HISTOGRAM_COUNTS_100000(
        "Spellcheck.DocumentCustomDictionary.WordCount",
        static_cast<int>(document_custom_words_.size()));
  }
  delete this;
}
