// Copyright 2020 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/omnibox/browser/zero_suggest_verbatim_match_provider.h"

#include <string>

#include "base/strings/escape.h"
#include "base/strings/utf_string_conversions.h"
#include "build/android_buildflags.h"
#include "components/dom_distiller/core/url_utils.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/history_types.h"
#include "components/history/core/browser/url_database.h"
#include "components/history/core/browser/url_row.h"
#include "components/omnibox/browser/autocomplete_enums.h"
#include "components/omnibox/browser/autocomplete_match_classification.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/page_classification_functions.h"
#include "components/omnibox/browser/suggestion_group_util.h"
#include "components/omnibox/browser/verbatim_match.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/search_engines/template_url_service.h"
#include "components/url_formatter/url_formatter.h"
#include "ui/base/device_form_factor.h"

namespace {
constexpr bool is_android = !!BUILDFLAG(IS_ANDROID);
constexpr bool is_desktop_android = !!BUILDFLAG(IS_DESKTOP_ANDROID);

// Returns whether specific context is eligible for a verbatim match.
// Offers verbatim match for:
// 1) a site visit and SRP (no NTP etc).
// 2) A composebox.
bool IsVerbatimMatchEligible(
    metrics::OmniboxEventProto::PageClassification context) {
  using OEP = metrics::OmniboxEventProto;

  if (context == OEP::ANDROID_SEARCH_WIDGET ||
      context == OEP::ANDROID_SHORTCUTS_WIDGET ||
      omnibox::IsComposebox(context)) {
    return true;
  }

  if (is_desktop_android) {
    return false;
  }

  return context == OEP::SEARCH_RESULT_PAGE_DOING_SEARCH_TERM_REPLACEMENT ||
         context == OEP::SEARCH_RESULT_PAGE_NO_SEARCH_TERM_REPLACEMENT ||
         context == OEP::SEARCH_RESULT_PAGE_ON_CCT ||
         context == OEP::OTHER_ON_CCT || context == OEP::OTHER;
}
}  // namespace

ZeroSuggestVerbatimMatchProvider::ZeroSuggestVerbatimMatchProvider(
    AutocompleteProviderClient* client)
    : AutocompleteProvider(TYPE_VERBATIM_MATCH), client_(client) {}

ZeroSuggestVerbatimMatchProvider::~ZeroSuggestVerbatimMatchProvider() = default;

void ZeroSuggestVerbatimMatchProvider::Start(const AutocompleteInput& input,
                                             bool minimal_changes) {
  Stop(AutocompleteStopReason::kClobbered);
  if (!IsVerbatimMatchEligible(input.current_page_classification()))
    return;

  // Only offer verbatim match after the user just focused the Omnibox on NTP,
  // SRP, or existing website view, or if the input field is empty.
  if (!input.IsZeroSuggest()) {
    return;
  }

  // For consistency with other zero-prefix providers.
  const auto& page_url = input.current_url();
  if (input.type() != metrics::OmniboxInputType::EMPTY &&
      !(page_url.is_valid() &&
        ((page_url.scheme() == url::kHttpScheme) ||
         (page_url.scheme() == url::kHttpsScheme) ||
         (page_url.scheme() == url::kAboutScheme) ||
         (page_url.scheme() ==
          client_->GetEmbedderRepresentationOfAboutScheme())))) {
    return;
  }

  // For composebox only, create verbatim match if there are context inputs
  // in zero suggest.
  if (omnibox::IsComposebox(input.current_page_classification())) {
    if (base::FeatureList::IsEnabled(
            omnibox::kComposeboxVerbatimMatchZeroSuggest) &&
        input.lens_overlay_suggest_inputs().has_value()) {
      CreateVerbatimMatchForComposebox(input);
    }
    return;
  }

  CreateVerbatimMatch(input, input.current_title());

  // It is possible for `title` to be empty if the page is currently loading.
  // If title is empty and async matches are permitted, make an effort to
  // retrieve page title from history database.
  if (!matches_.back().description.empty() ||
      input.omit_asynchronous_matches()) {
    return;
  }

  history::HistoryService* const history_service = client_->GetHistoryService();
  if (!history_service) {
    return;
  }

  // Attempt to retrieve `title` from historical records.
  done_ = false;
  history_service->QueryURL(
      input.current_url(),
      base::BindOnce(&ZeroSuggestVerbatimMatchProvider::OnPageTitleRetrieved,
                     request_weak_ptr_factory_.GetWeakPtr(), input),
      &task_tracker_);
}

void ZeroSuggestVerbatimMatchProvider::Stop(
    AutocompleteStopReason stop_reason) {
  AutocompleteProvider::Stop(stop_reason);
  request_weak_ptr_factory_.InvalidateWeakPtrs();
}

void ZeroSuggestVerbatimMatchProvider::OnPageTitleRetrieved(
    const AutocompleteInput& input,
    history::QueryURLResult result) {
  done_ = true;
  // Re-create the item with a title collected from History service.
  matches_.clear();
  CreateVerbatimMatch(input, result.row.title());
}

void ZeroSuggestVerbatimMatchProvider::CreateVerbatimMatchForComposebox(
    const AutocompleteInput& input) {
  AutocompleteInput verbatim_input = input;
  verbatim_input.set_prevent_inline_autocomplete(true);
  verbatim_input.set_allow_exact_keyword_match(false);

  AutocompleteMatch match =
      VerbatimMatchForContext(this, client_, verbatim_input,
                              omnibox::kVerbatimMatchZeroSuggestRelevance);

  // Will only be valid if there's a TemplateURLService and DSE.
  if (match.destination_url.is_valid()) {
    match.suggestion_group_id = omnibox::GROUP_SEARCH;
    matches_.push_back(std::move(match));
  }
}

void ZeroSuggestVerbatimMatchProvider::CreateVerbatimMatch(
    const AutocompleteInput& input,
    std::u16string page_title) {
  AutocompleteInput verbatim_input = input;
  verbatim_input.set_prevent_inline_autocomplete(true);
  verbatim_input.set_allow_exact_keyword_match(false);

  AutocompleteMatch match = VerbatimMatchForURL(
      this, client_, verbatim_input, input.current_url(), std::move(page_title),
      omnibox::kVerbatimMatchZeroSuggestRelevance);
  // Make sure the URL is formatted the same was as most visited sites.
  auto format_types = AutocompleteMatch::GetFormatTypes(false, false);
  match.suggestion_group_id = omnibox::GROUP_MOBILE_SEARCH_READY_OMNIBOX;
  match.contents = url_formatter::FormatUrl(input.current_url(), format_types,
                                            base::UnescapeRule::SPACES, nullptr,
                                            nullptr, nullptr);

  TermMatches term_matches;
  if (input.text().length() > 0) {
    term_matches = {{0, 0, input.text().length()}};
  }

  match.contents_class = ClassifyTermMatches(
      term_matches, match.contents.size(),
      ACMatchClassification::MATCH | ACMatchClassification::URL,
      ACMatchClassification::URL);

  // If the URL suggestion comes from the default search engine, extract the
  // original search query and place it in fill_into_edit, to permit re-use of
  // the query for manual refinement.
  if constexpr (is_android) {
    auto* const url_service = client_->GetTemplateURLService();
    if (url_service->IsSearchResultsPageFromDefaultSearchProvider(
            match.destination_url)) {
      auto* const dse = url_service->GetDefaultSearchProvider();
      if (dse->url_ref().SupportsReplacement(
              url_service->search_terms_data())) {
        dse->ExtractSearchTermsFromURL(match.destination_url,
                                       url_service->search_terms_data(),
                                       &match.contents);
        match.contents = AutocompleteInput::SanitizeString(match.contents);
        // Upgrade Verbatim Match to a SEARCH_WHAT_YOU_TYPED.
        match.type = AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED;
        match.keyword = dse->keyword();
        match.fill_into_edit = match.contents;
        if (match.description.empty() ||
            match.description ==
                base::UTF8ToUTF16(match.destination_url.spec())) {
          match.description = match.fill_into_edit;
          if (match.description_class.empty()) {
            match.description_class.push_back({0, ACMatchClassification::NONE});
          }
        }
      }
    } else {
      // URL suggestion here does not come from the default search engine.
      // Ensure that distilled URL is transformed to original URL for
      // fill_into_edit and contents fields of the suggestion.
      if (dom_distiller::url_utils::IsDistilledPage(match.destination_url)) {
        GURL original_url =
            dom_distiller::url_utils::GetOriginalUrlFromDistillerUrl(
                match.destination_url);
        match.fill_into_edit = base::UTF8ToUTF16(original_url.spec());
        match.contents = url_formatter::FormatUrl(
            original_url,
            url_formatter::kFormatUrlOmitDefaults |
                url_formatter::kFormatUrlOmitHTTPS |
                url_formatter::kFormatUrlOmitTrivialSubdomains,
            base::UnescapeRule::SPACES, nullptr, nullptr, nullptr);
      }
    }
  }

  match.provider = this;
  matches_.push_back(std::move(match));
}
