// Copyright 2017 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/titled_url_match_utils.h"

#include <numeric>
#include <string_view>
#include <vector>

#include "base/metrics/field_trial_params.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "components/bookmarks/browser/titled_url_node.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_match_classification.h"
#include "components/omnibox/browser/autocomplete_scoring_signals_annotator.h"
#include "components/omnibox/browser/history_provider.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/url_prefix.h"
#include "components/query_parser/snippet.h"
#include "components/url_formatter/url_formatter.h"
#include "third_party/metrics_proto/omnibox_scoring_signals.pb.h"

namespace bookmarks {
namespace {

using ScoringSignals = ::metrics::OmniboxScoringSignals;

// Concatenates |ancestors| in reverse order and using '/' as the delimiter.
std::u16string ConcatAncestorsTitles(
    std::vector<std::u16string_view> ancestors) {
  return ancestors.empty()
             ? std::u16string()
             : std::accumulate(
                   std::next(ancestors.rbegin()), ancestors.rend(),
                   std::u16string(*ancestors.rbegin()),
                   [](const std::u16string& a, std::u16string_view b) {
                     return a + u"/" + std::u16string(b);
                   });
}

}  // namespace

AutocompleteMatch TitledUrlMatchToAutocompleteMatch(
    const TitledUrlMatch& titled_url_match,
    AutocompleteMatchType::Type type,
    int relevance,
    int bookmark_count,
    AutocompleteProvider* provider,
    const AutocompleteSchemeClassifier& scheme_classifier,
    const AutocompleteInput& input,
    const std::u16string& fixed_up_input_text) {
  const std::u16string title = AutocompleteMatch::SanitizeString(
      titled_url_match.node->GetTitledUrlNodeTitle());
  const GURL& url = titled_url_match.node->GetTitledUrlNodeUrl();
  const std::u16string path =
      AutocompleteMatch::SanitizeString(ConcatAncestorsTitles(
          titled_url_match.node->GetTitledUrlNodeAncestorTitles()));

  // The AutocompleteMatch we construct is non-deletable because the only way to
  // support this would be to delete the underlying object that created the
  // titled_url_match. E.g., for the bookmark provider this would mean deleting
  // the underlying bookmark, which is unlikely to be what the user intends.
  AutocompleteMatch match(provider, relevance, false, type);
  match.destination_url = url;
  match.RecordAdditionalInfo("Title", title);
  match.RecordAdditionalInfo("URL", url.spec());
  match.RecordAdditionalInfo("Path", path);

  bool match_in_scheme = false;
  bool match_in_subdomain = false;
  AutocompleteMatch::GetMatchComponents(url,
                                        titled_url_match.url_match_positions,
                                        &match_in_scheme, &match_in_subdomain);
  auto format_types = AutocompleteMatch::GetFormatTypes(
      input.parts().scheme.is_nonempty() || match_in_scheme,
      match_in_subdomain);
  const std::u16string formatted_url = url_formatter::FormatUrl(
      url, format_types, base::UnescapeRule::SPACES, nullptr, nullptr, nullptr);

  // Display the URL only if the input matches the URL but not the path.
  // Otherwise, display the path, even if the input matches both or neither.
  bool show_path = titled_url_match.has_ancestor_match ||
                   titled_url_match.url_match_positions.empty();
  match.contents = show_path ? path : formatted_url;
  // The path can become stale (when the bookmark is moved). So persist the URL
  // instead when creating shortcuts.
  if (show_path)
    match.description_for_shortcuts = formatted_url;

  // Bookmark classification diverges from relevance scoring. Specifically,
  // 1) All occurrences of the input contribute to relevance; e.g. for the input
  // 'pre', the bookmark 'pre prefix' will be scored higher than 'pre suffix'.
  // For classification though, if the input is a prefix of the suggestion text,
  // only the prefix will be bolded; e.g. the 1st bookmark will display '[pre]
  // prefix' as opposed to '[pre] [pre]fix'. This divergence allows consistency
  // with other providers' and google.com's bolding.
  // 2) Non-complete-word matches less than 3 characters long do not contribute
  // to relevance; e.g. for the input 'a pr', the bookmark 'a pr prefix' will be
  // scored the same as 'a pr suffix'. For classification though, both
  // occurrences will be bolded, 'a [pr] [pr]efix'.
  auto contents_terms = FindTermMatches(input.text(), match.contents);
  match.contents_class = ClassifyTermMatches(
      contents_terms, match.contents.length(),
      ACMatchClassification::MATCH | ACMatchClassification::URL,
      ACMatchClassification::URL);

  if (show_path) {
    auto terms = FindTermMatches(input.text(), match.description_for_shortcuts);
    match.description_class_for_shortcuts = ClassifyTermMatches(
        terms, match.description_for_shortcuts.length(),
        ACMatchClassification::MATCH | ACMatchClassification::URL,
        ACMatchClassification::URL);
  }

  match.description = title;
  auto description_terms = FindTermMatches(input.text(), match.description);
  match.description_class = ClassifyTermMatches(
      description_terms, match.description.length(),
      ACMatchClassification::MATCH, ACMatchClassification::NONE);

  // The inline_autocomplete_offset should be adjusted based on the formatting
  // applied to |fill_into_edit|.
  size_t inline_autocomplete_offset = URLPrefix::GetInlineAutocompleteOffset(
      input.text(), fixed_up_input_text, false, base::UTF8ToUTF16(url.spec()));
  auto fill_into_edit_format_types = url_formatter::kFormatUrlOmitDefaults;
  if (match_in_scheme)
    fill_into_edit_format_types &= ~url_formatter::kFormatUrlOmitHTTP;
  match.fill_into_edit =
      AutocompleteInput::FormattedStringWithEquivalentMeaning(
          url,
          url_formatter::FormatUrl(url, fill_into_edit_format_types,
                                   base::UnescapeRule::SPACES, nullptr, nullptr,
                                   &inline_autocomplete_offset),
          scheme_classifier, &inline_autocomplete_offset);

  if (match.TryRichAutocompletion(input, match.contents, match.description)) {
    // If rich autocompletion applies, we skip trying the alternatives below.
  } else if (inline_autocomplete_offset != std::u16string::npos) {
    match.inline_autocompletion =
        match.fill_into_edit.substr(inline_autocomplete_offset);
    match.SetAllowedToBeDefault(input);
  }

  if (input.in_keyword_mode()) {
    match.from_keyword = true;
  }

  if (OmniboxFieldTrial::IsPopulatingUrlScoringSignalsEnabled() &&
      match.IsMlSignalLoggingEligible()) {
    match.scoring_signals = std::make_optional<ScoringSignals>();
    // Populate ACMatches with signals for ML model scoring and training.
    if (!titled_url_match.title_match_positions.empty())
      match.scoring_signals->set_first_bookmark_title_match_position(
          titled_url_match.title_match_positions[0].first);
    match.scoring_signals->set_total_bookmark_title_match_length(
        GetTotalTitleMatchLength(titled_url_match.title_match_positions));
    match.scoring_signals->set_allowed_to_be_default_match(
        match.allowed_to_be_default_match);
    match.scoring_signals->set_length_of_url(url.spec().length());
    match.scoring_signals->set_num_bookmarks_of_url(bookmark_count);
  }

  return match;
}

// Computes the total length of matched strings in the bookmark title.
int GetTotalTitleMatchLength(
    const query_parser::Snippet::MatchPositions& title_match_positions) {
  int len = 0;
  for (const auto& title_match : title_match_positions) {
    len += title_match.second - title_match.first;
  }
  return len;
}

}  // namespace bookmarks
