// Copyright 2022 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/ash/app_list/search/omnibox/omnibox_result.h"

#include <optional>
#include <string>
#include <utility>

#include "ash/public/cpp/app_list/vector_icons/vector_icons.h"
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/check_deref.h"
#include "base/metrics/field_trial_params.h"
#include "base/strings/strcat.h"
#include "chrome/browser/ash/app_list/app_list_controller_delegate.h"
#include "chrome/browser/ash/app_list/search/common/icon_constants.h"
#include "chrome/browser/ash/app_list/search/common/search_result_util.h"
#include "chrome/browser/ash/app_list/search/omnibox/omnibox_types.h"
#include "chrome/browser/ash/app_list/search/omnibox/omnibox_util.h"
#include "chrome/browser/bitmap_fetcher/bitmap_fetcher.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/string_matching/fuzzy_tokenized_string_match.h"
#include "components/omnibox/browser/favicon_cache.h"
#include "components/omnibox/browser/vector_icons.h"
#include "components/search_engines/util.h"
#include "components/strings/grit/components_strings.h"
#include "extensions/common/image_util.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/window_open_disposition_utils.h"
#include "ui/gfx/paint_vector_icon.h"
#include "url/gurl.h"

namespace app_list {
namespace {

using ::ash::string_matching::FuzzyTokenizedStringMatch;
using ::ash::string_matching::TokenizedString;

// Parameters for FuzzyTokenizedStringMatch.
constexpr bool kUseWeightedRatio = false;

constexpr double kRelevanceThreshold = 0.32;

// Flag to enable/disable diacritics stripping.
constexpr bool kStripDiacritics = true;

// Flag to enable/disable acronym matcher.
constexpr bool kUseAcronymMatcher = true;

// Priority numbers for deduplication. Higher numbers indicate higher priority.
constexpr int kRichEntityPriority = 2;
constexpr int kHistoryPriority = 1;
constexpr int kDefaultPriority = 0;

// OmniboxResultType to vector icon, used for app list.
const gfx::VectorIcon& TypeToVectorIcon(OmniboxResultType type) {
  switch (type) {
    case OmniboxResultType::kDomain:
      return ash::kOmniboxGenericIcon;
    case OmniboxResultType::kSearch:
      return ash::kSearchIcon;
    case OmniboxResultType::kHistory:
      return ash::kHistoryIcon;
    default:
      NOTREACHED();
  }
}

}  // namespace

OmniboxResult::OmniboxResult(Profile* profile,
                             AppListControllerDelegate* list_controller,
                             TemplateURLService* template_url_service,
                             std::unique_ptr<OmniboxResultData> search_result,
                             const std::u16string& query,
                             FaviconCache* favicon_cache)
    : profile_(profile),
      list_controller_(list_controller),
      template_url_service_(CHECK_DEREF(template_url_service)),
      search_result_(std::move(search_result)),
      query_(query),
      contents_(search_result_->contents.value_or(u"")),
      description_(search_result_->description.value_or(u"")) {
  CHECK(favicon_cache);

  SetDisplayType(DisplayType::kList);
  SetResultType(ResultType::kOmnibox);
  SetMetricsType(search_result_->metrics_type);

  set_id(search_result_->stripped_destination_url.spec());

  // Omnibox results are categorized as Search and Assistant if they are search
  // suggestions, and Web otherwise.
  SetCategory(search_result_->omnibox_type == OmniboxResultType::kSearch
                  ? Category::kSearchAndAssistant
                  : Category::kWeb);

  if (IsRichEntity()) {
    dedup_priority_ = kRichEntityPriority;
  } else if (search_result_->omnibox_type == OmniboxResultType::kHistory) {
    dedup_priority_ = kHistoryPriority;
  } else {
    dedup_priority_ = kDefaultPriority;
  }

  SetSkipUpdateAnimation(search_result_->metrics_type ==
                         ash::OMNIBOX_WEB_QUERY);

  FetchFavicon(favicon_cache);
  UpdateIcon();
  UpdateTitleAndDetails();

  // Requires the title for fuzzy match and thus must be calculated after the
  // title is set.
  UpdateRelevance();

  if (search_result_->is_omnibox_search) {
    InitializeButtonActions({ash::SearchResultActionType::kRemove});
  }

  if (auto* dark_light_mode_controller = ash::DarkLightModeController::Get())
    dark_light_mode_controller->AddObserver(this);
}

OmniboxResult::~OmniboxResult() {
  if (auto* dark_light_mode_controller = ash::DarkLightModeController::Get())
    dark_light_mode_controller->RemoveObserver(this);
}

void OmniboxResult::UpdateRelevance() {
  double normalized_autocomplete_relevance =
      search_result_->relevance / kMaxOmniboxScore;

  double title_relevance = CalculateTitleRelevance();
  if (title_relevance < kRelevanceThreshold) {
    scoring().set_filtered(true);
  }

  // Derive relevance from autocomplete relevance and normalize it to [0, 1].
  set_relevance(normalized_autocomplete_relevance);
}

double OmniboxResult::CalculateTitleRelevance() const {
  const TokenizedString tokenized_title(title(), TokenizedString::Mode::kWords);
  const TokenizedString tokenized_query(query_,
                                        TokenizedString::Mode::kCamelCase);

  if (tokenized_query.text().empty() || tokenized_title.text().empty()) {
    static constexpr double kDefaultRelevance = 0.0;
    return kDefaultRelevance;
  }

  FuzzyTokenizedStringMatch match;
  return match.Relevance(tokenized_query, tokenized_title, kUseWeightedRatio,
                         kStripDiacritics, kUseAcronymMatcher);
}

std::optional<GURL> OmniboxResult::url() const {
  return search_result_->destination_url;
}

void OmniboxResult::Open(int event_flags) {
  list_controller_->OpenURL(profile_, search_result_->destination_url,
                            search_result_->page_transition,
                            ui::DispositionFromEventFlags(event_flags));
}

void OmniboxResult::OnColorModeChanged(bool dark_mode_enabled) {
  if (uses_generic_icon_)
    SetGenericIcon();
}

void OmniboxResult::FetchFavicon(FaviconCache* favicon_cache) {
  CHECK(favicon_cache);
  CHECK(search_result_->favicon.isNull());
  if (IsEligibleForFavicon(search_result_->omnibox_type)) {
    gfx::Image favicon = favicon_cache->GetFaviconForPageUrl(
        search_result_->destination_url,
        base::BindOnce(&OmniboxResult::OnFetchedFavicon,
                       weak_factory_.GetWeakPtr()));
    if (!favicon.IsEmpty()) {
      OnFetchedFavicon(favicon);
    }
  }
}

void OmniboxResult::OnFetchedFavicon(const gfx::Image& icon) {
  CHECK(search_result_->favicon.isNull());
  auto image_skia = icon.AsImageSkia();
  search_result_->favicon = image_skia;
  CHECK(!search_result_->favicon.isNull());
  SetIcon(
      IconInfo(ui::ImageModel::FromImageSkia(image_skia), kFaviconDimension));
}

void OmniboxResult::UpdateIcon() {
  if (IsRichEntity()) {
    FetchRichEntityImage(*search_result_->image_url);
    return;
  }

  // If we already have a favicon, use that.
  // See also FetchFavicon, which takes care of the case where the favicon
  // becomes available later.
  gfx::ImageSkia icon = search_result_->favicon;
  if (!icon.isNull()) {
    SetIcon(IconInfo(ui::ImageModel::FromImageSkia(icon), kFaviconDimension));
    return;
  }

  SetGenericIcon();
}

void OmniboxResult::SetGenericIcon() {
  uses_generic_icon_ = true;
  // If this is neither a rich entity nor eligible for a favicon, use either
  // the generic bookmark or another generic icon as appropriate.
  if (search_result_->omnibox_type == OmniboxResultType::kBookmark) {
    SetIcon(IconInfo(
        ui::ImageModel::FromVectorIcon(
            features::IsRoundedIconsEnabled() ? omnibox::kStarsFilledIcon
                                              : omnibox::kBookmarkOldIcon,
            kGenericIconColorId, kSystemIconDimension),

        kSystemIconDimension));
  } else {
    SetIcon(IconInfo(ui::ImageModel::FromVectorIcon(
                         TypeToVectorIcon(search_result_->omnibox_type),
                         kGenericIconColorId, kSystemIconDimension),
                     kSystemIconDimension));
  }
}

void OmniboxResult::UpdateTitleAndDetails() {
  if (!IsUrlResultWithDescription()) {
    SetTitle(contents_);
    SetTitleTags(TagsForText(contents_, search_result_->contents_type));

    if (IsRichEntity()) {
      // Append the search engine to the description.
      const std::u16string description_with_search_context =
          l10n_util::GetStringFUTF16(
              IDS_APP_LIST_QUERY_SEARCH_DESCRIPTION, description_,
              GetDefaultSearchEngineName(
                  &template_url_service_.get()));
      SetDetails(description_with_search_context);
      SetDetailsTags(
          TagsForText(description_, search_result_->description_type));

      // Append the search engine to the accessible name.
      const std::u16string accessible_name =
          details().empty() ? title()
                            : base::StrCat({title(), u", ", details()});
      SetAccessibleName(l10n_util::GetStringFUTF16(
          IDS_APP_LIST_QUERY_SEARCH_ACCESSIBILITY_NAME, accessible_name,
          GetDefaultSearchEngineName(
              &template_url_service_.get())));
    } else if (search_result_->is_omnibox_search) {
      // For non-rich-entity results, put the search engine into the details
      // field. Tags are not used since this does not change with the query.
      SetDetails(l10n_util::GetStringFUTF16(
          IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION,
          GetDefaultSearchEngineName(
              &template_url_service_.get())));
    }
  } else {
    // For url result with non-empty description, swap title and details. Thus,
    // the url description is presented as title, and url itself is presented as
    // details.
    SetTitle(description_);
    SetTitleTags(TagsForText(description_, search_result_->description_type));

    SetDetails(contents_);
    SetDetailsTags(TagsForText(contents_, search_result_->contents_type));
  }
}

bool OmniboxResult::IsUrlResultWithDescription() const {
  return !(search_result_->is_omnibox_search || description_.empty());
}

bool OmniboxResult::IsRichEntity() const {
  return search_result_->image_url.value_or(GURL()).is_valid();
}

void OmniboxResult::FetchRichEntityImage(const GURL& url) {
  if (!bitmap_fetcher_) {
    bitmap_fetcher_ =
        std::make_unique<BitmapFetcher>(url, this, kOmniboxTrafficAnnotation);
  }
  bitmap_fetcher_->Init(net::ReferrerPolicy::NEVER_CLEAR,
                        network::mojom::CredentialsMode::kOmit);
  bitmap_fetcher_->Start(profile_->GetURLLoaderFactory().get());
}

void OmniboxResult::OnFetchComplete(const GURL& url, const SkBitmap* bitmap) {
  if (!bitmap)
    return;

  IconInfo icon_info(ui::ImageModel::FromImageSkia(
                         gfx::ImageSkia::CreateFrom1xBitmap(*bitmap)),
                     kImageIconDimension, IconShape::kRoundedRectangle);
  SetIcon(icon_info);
}

void OmniboxResult::InitializeButtonActions(
    const std::vector<ash::SearchResultActionType>& button_actions) {
  Actions actions;
  for (ash::SearchResultActionType button_action : button_actions) {
    std::u16string button_tooltip;
    switch (button_action) {
      case ash::SearchResultActionType::kRemove:
        button_tooltip = l10n_util::GetStringFUTF16(
            IDS_APP_LIST_REMOVE_SUGGESTION_ACCESSIBILITY_NAME, title());
        break;
    }
    Action search_action(button_action, button_tooltip);
    actions.emplace_back(search_action);
  }

  SetActions(actions);
}

}  // namespace app_list
