// 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 "autocomplete_controller_metrics.h"

#include <string>
#include <string_view>

#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "components/omnibox/browser/autocomplete_controller.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/autocomplete_result.h"
#include "third_party/metrics_proto/omnibox_event.pb.h"

#if !BUILDFLAG(IS_IOS)
#include "base/feature_list.h"
#include "components/omnibox/browser/geolocation_header_service.h"
#include "components/omnibox/common/omnibox_features.h"
#endif

namespace {

enum class MetricNameSuffix {
  kDone,
  kLastChange,
  kLastDefaultChange,
  kProvider,
  kMaxValue = kProvider
};

constexpr std::string_view ToString(MetricNameSuffix name_suffix) {
  using enum MetricNameSuffix;
  switch (name_suffix) {
    case kDone:
      return "Done";
    case kLastChange:
      return "LastChange";
    case kLastDefaultChange:
      return "LastDefaultChange";
    case kProvider:
      return "Provider";
  }
  NOTREACHED();
}

std::string GetMetricName(MetricNameSuffix name_suffix,
                          const AutocompleteProvider* provider,
                          std::string_view completion_suffix = "") {
  return base::StrCat(
      {"Omnibox.AsyncAutocompletionTime2.", ToString(name_suffix),
       (provider ? "." : ""), (provider ? provider->GetName() : ""),
       (completion_suffix.empty() ? "" : "."), completion_suffix});
}

void LogAsyncAutocompletionTimeMetricsImpl(MetricNameSuffix name_suffix,
                                           const AutocompleteProvider* provider,
                                           bool completed,
                                           base::TimeDelta elapsed_time) {
  // This may be called up to 40 times per omnibox key-stroke. Cache the
  // histograms in a lookup table keyed  by name_suffix + provider_number
  // (where provider_number is 0 unless name_suffix == kProvider) so that
  // we can avoid having to construct their names and look them up each time.

  // The max size of each of the histogram tables.
  constexpr int kMaxHistogramSize =
      (static_cast<int>(MetricNameSuffix::kMaxValue) +
       metrics::OmniboxEventProto_ProviderType_ProviderType_MAX) +
      1;

  // Validate the histogram lookup parameters:
  // * name_suffix is in [0..kMaxValue]
  // * `provider` is non-null iff name_suffix == kProvider, and vice versa.
  // * if non-null, provider yields a value <= ProviderType_MAX
  DCHECK_GE(static_cast<int>(name_suffix), 0);
  DCHECK_LE(static_cast<int>(name_suffix),
            static_cast<int>(MetricNameSuffix::kMaxValue));
  DCHECK_EQ(name_suffix == MetricNameSuffix::kProvider, !!provider);
  DCHECK(!provider ||
         provider->AsOmniboxEventProviderType() <=
             metrics::OmniboxEventProto_ProviderType_ProviderType_MAX);

  // Each histogram is at the same index in its respective table.
  const int histogram_index =
      static_cast<int>(name_suffix) +
      (provider ? provider->AsOmniboxEventProviderType() : 0);

  // Use the `STATIC_HISTOGRAM_POINTER_GROUP` macro to define a static table of
  // atomic histogram pointers which is indexed by `histogram_index`.
  //
  // I.e., the histograms are ordered as:
  //   Done, LastChange, DefaultChange, Provider-0, Provider-1, ...
#define STATIC_HISTOGRAM_TIMES_POINTER_GROUP(name, sample)    \
  STATIC_HISTOGRAM_POINTER_GROUP(                             \
      name, histogram_index, kMaxHistogramSize,               \
      AddTimeMillisecondsGranularity(sample),                 \
      base::Histogram::FactoryTimeGet(                        \
          name, base::Milliseconds(1), base::Seconds(10), 50, \
          base::HistogramBase::kUmaTargetedHistogramFlag))

  // These metrics are logged up to about 40 times each per omnibox keystroke.
  // `GetMetricName()` is deterministic for any given set of parameters, so each
  // histogram name is a run-time constant and a pointer to the corresponding
  // histogram object will be cached on first use in a static table.
  STATIC_HISTOGRAM_TIMES_POINTER_GROUP(GetMetricName(name_suffix, provider),
                                       elapsed_time);
  if (completed) {
    STATIC_HISTOGRAM_TIMES_POINTER_GROUP(
        GetMetricName(name_suffix, provider, "Completed"), elapsed_time);
  } else {
    STATIC_HISTOGRAM_TIMES_POINTER_GROUP(
        GetMetricName(name_suffix, provider, "Interrupted"), elapsed_time);
  }
#undef STATIC_HISTOGRAM_TIMES_POINTER_GROUP
}

inline void LogAsyncAutocompletionTimeMetrics(MetricNameSuffix metric,
                                              bool completed,
                                              base::TimeDelta elapsed_time) {
  DCHECK_NE(metric, MetricNameSuffix::kProvider);
  LogAsyncAutocompletionTimeMetricsImpl(metric, nullptr, completed,
                                        elapsed_time);
}

inline void LogAsyncAutocompletionTimeMetrics(
    const AutocompleteProvider& provider,
    base::TimeDelta elapsed_time) {
  LogAsyncAutocompletionTimeMetricsImpl(MetricNameSuffix::kProvider, &provider,
                                        provider.done(), elapsed_time);
}

inline bool IsSynchronousUpdate(
    AutocompleteController::UpdateType update_type) {
  return update_type == AutocompleteController::UpdateType::kSyncPass ||
         update_type == AutocompleteController::UpdateType::kSyncPassOnly;
}

}  // namespace

AutocompleteControllerMetrics::AutocompleteControllerMetrics(
    const AutocompleteController& controller)
    : controller_(controller) {}

void AutocompleteControllerMetrics::OnStart() {
  OnStop();
  logged_finalization_metrics_ = false;
  start_time_ = base::TimeTicks::Now();
  last_change_time_ = start_time_;
  last_default_change_time_ = start_time_;
}

void AutocompleteControllerMetrics::OnNotifyChanged(
    std::vector<AutocompleteResult::MatchDedupComparator>
        last_result_comparators,
    const AutocompleteResult& new_result) {
  // Only log metrics for async requests.
  if (controller_->input().omit_asynchronous_matches()) {
    return;
  }

  // If results are empty then the omnibox is likely closed, and clearing old
  // results won't be user visible. E.g., this occurs when opening a new tab
  // while the popup was open.
  if (new_result.empty()) {
    return;
  }

  std::vector<AutocompleteResult::MatchDedupComparator> new_result_comparators =
      new_result.GetMatchDedupComparators();

  // Log suggestion changes.

  bool any_match_changed_or_removed = false;
  for (size_t i = 0; i < last_result_comparators.size(); ++i) {
    // Log changed or removed matches. Don't log for matches appended to the
    // bottom since that's less disruptive.
    if (i >= new_result_comparators.size() ||
        last_result_comparators[i] != new_result_comparators[i]) {
      LogSuggestionChangeIndexMetrics(i);
      any_match_changed_or_removed = true;
    }
  }
  LogSuggestionChangeInAnyPositionMetrics(any_match_changed_or_removed);

  // Log suggestion finalization times. This handles logging as soon as the
  // final update occurs, while `OnStop()` handles the case where the final
  // update never occurs because of interruptions.

  // E.g., suggestion deletion can call `OnNotifyChanged()` after the controller
  // is done and finalization metrics have been logged. They should not be
  // logged again.
  if (controller_->last_update_type() ==
      AutocompleteController::UpdateType::kMatchDeletion) {
    return;
  }

  const bool any_match_changed_or_removed_or_added =
      any_match_changed_or_removed ||
      last_result_comparators.size() != new_result_comparators.size();
  const bool default_match_changed_or_removed_or_added =
      last_result_comparators.empty() ||
      last_result_comparators[0] != new_result_comparators[0];

  if (any_match_changed_or_removed_or_added) {
    last_change_time_ = base::TimeTicks::Now();
  }
  if (default_match_changed_or_removed_or_added) {
    DCHECK(any_match_changed_or_removed_or_added);
    last_default_change_time_ = last_change_time_;
  }
  // It's common to have multiple async updates per input. Only log the final
  // update.
  // TODO(crbug.com/364303536): `logged_finalization_metrics_` should be
  //   guaranteed false here (hence the DCHECK in
  //   `LogSuggestionFinalizationMetrics()`. But because of a temporary band-aid
  //   to allow history embedding answers and unscoped extension suggestions to
  //   ignore the stop timer, we need to check it anyways.
  if (controller_->done() && !logged_finalization_metrics_) {
    LogSuggestionFinalizationMetrics(new_result);
  }
}

void AutocompleteControllerMetrics::OnProviderUpdate(
    const AutocompleteProvider& provider) const {
  // Only log metrics for async requests. This will likely never happen, since
  // `OnProviderUpdate()` is only called by async providers (but not necessarily
  // async'ly, see the comments in `AutocompleteController::OnProviderUpdate`).
  if (controller_->input().omit_asynchronous_matches()) {
    return;
  }

  // Some async providers may produce multiple updates. Only log the final async
  // update.
  if (provider.done()) {
    LogProviderTimeMetrics(provider);
  }
}

void AutocompleteControllerMetrics::OnStop() {
  // Only log metrics for async requests.
  if (controller_->input().omit_asynchronous_matches()) {
    return;
  }

  // Done providers should already be logged by `OnProviderUpdate()`.
  for (const auto& provider : controller_->providers()) {
    if (!provider->done()) {
      LogProviderTimeMetrics(*provider);
    }
  }

  // If the controller is done, `OnNotifyChanged()` should have already
  // logged finalization metrics. This case, i.e. `OnStop()` invoked even
  // though the controller is done, is possible because `OnStart()` calls
  // `OnStop()`.
  // TODO(crbug.com/364303536): `logged_finalization_metrics_` should be
  //   guaranteed false here (hence the DCHECK in
  //   `LogSuggestionFinalizationMetrics()`. But because of a temporary
  //   bandaid to allow history embedding answers and unscoped extension
  //   answers to ignore the stop timer, we need to check it anyways.
  if (!controller_->done() && !logged_finalization_metrics_) {
    LogSuggestionFinalizationMetrics(controller_->result());
  }
}

void AutocompleteControllerMetrics::LogSuggestionFinalizationMetrics(
    const AutocompleteResult& result) {
  // Finalization metrics should be logged once only, either when all
  // async providers complete or they're interrupted before completion.
#if BUILDFLAG(IS_IOS)
  // iOS is weird in that it sometimes calls `InjectAdHocMatch()` when the user
  // selects a suggestion, thus changing the results when autocompletion is done
  // and suggestions should be stable.
  if (logged_finalization_metrics_)
    return;
#endif
  DCHECK(!logged_finalization_metrics_)
      << "last_update_type: "
      << AutocompleteController::UpdateTypeToDebugString(
             controller_->last_update_type());
  logged_finalization_metrics_ = true;

  const auto done_elapsed_time = base::TimeTicks::Now() - start_time_;
  const auto last_change_elapsed_time = last_change_time_ - start_time_;
  const auto last_default_change_elapsed_time =
      last_default_change_time_ - start_time_;
  const bool is_completed = controller_->done();

  using enum MetricNameSuffix;
  LogAsyncAutocompletionTimeMetrics(kDone, is_completed, done_elapsed_time);
  LogAsyncAutocompletionTimeMetrics(kLastChange, is_completed,
                                    last_change_elapsed_time);
  LogAsyncAutocompletionTimeMetrics(kLastDefaultChange, is_completed,
                                    last_default_change_elapsed_time);
#if !BUILDFLAG(IS_IOS)
  LogInlineLocationSuggestionMetrics(result);
#endif  // !BUILDFLAG(IS_IOS)
}

void AutocompleteControllerMetrics::LogProviderTimeMetrics(
    const AutocompleteProvider& provider) const {
  const auto elapsed_time = base::TimeTicks::Now() - start_time_;
  LogAsyncAutocompletionTimeMetrics(provider, elapsed_time);
}

void AutocompleteControllerMetrics::LogSuggestionChangeIndexMetrics(
    size_t change_index) const {
  // These metrics are logged up to about 50 times per omnibox keystroke, so use
  // the UMA macros (which cache the histogram pointer) for efficiency. All of
  // the histogram names used in this function are run-time constants.
  static constexpr char kName[] = "Omnibox.MatchStability2.MatchChangeIndex";
  constexpr size_t max = AutocompleteResult::kMaxAutocompletePositionValue;

  UMA_HISTOGRAM_EXACT_LINEAR(kName, change_index, max);
  if (IsSynchronousUpdate(controller_->last_update_type())) {
    UMA_HISTOGRAM_EXACT_LINEAR(base::StrCat({kName, ".CrossInput"}),
                                change_index, max);
  } else {
    UMA_HISTOGRAM_EXACT_LINEAR(base::StrCat({kName, ".Async"}), change_index,
                                max);
  }
}

void AutocompleteControllerMetrics::LogSuggestionChangeInAnyPositionMetrics(
    bool changed) const {
  // These metrics are logged up to about 5 times per omnibox keystroke, so use
  // the UMA macros (which cache the histogram pointer) for efficiency. All of
  // the histogram names used in this function are run-time constants.
  static constexpr char kName[] =
      "Omnibox.MatchStability2.MatchChangeInAnyPosition";

  UMA_HISTOGRAM_BOOLEAN(kName, changed);
  if (IsSynchronousUpdate(controller_->last_update_type())) {
    UMA_HISTOGRAM_BOOLEAN(base::StrCat({kName, ".CrossInput"}), changed);
  } else {
    UMA_HISTOGRAM_BOOLEAN(base::StrCat({kName, ".Async"}), changed);
  }
}

#if !BUILDFLAG(IS_IOS)
void AutocompleteControllerMetrics::LogInlineLocationSuggestionMetrics(
    const AutocompleteResult& result) const {
  if (!base::FeatureList::IsEnabled(omnibox::kInlineLocationSignaling)) {
    return;
  }

  const auto* geolocation_service = controller_->autocomplete_provider_client()
                                        ->GetGeolocationHeaderService();
  if (!geolocation_service) {
    return;
  }

  size_t position = 0;
  OmniboxInlineLocationSuggestionShown shown =
      OmniboxInlineLocationSuggestionShown::kNoEligibleSuggestionFound;

  for (size_t i = 0; i < result.size(); ++i) {
    const auto& match = result.match_at(i);
    if (match.extra_headers.contains(kXGeoHeader)) {
      shown = OmniboxInlineLocationSuggestionShown::kLocationSuggestionShown;
      position = i;
      break;
    }
    if (match.subtypes.contains(omnibox::SUBTYPE_LOCATION_SUGGEST_TRIGGER)) {
      shown = OmniboxInlineLocationSuggestionShown::kOnlyParentSuggestionShown;
    }
  }

  geolocation_service->RecordInlineLocationSuggestionShown(shown, position);
}
#endif
