// 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 "chrome/browser/metrics/tab_stats/tab_stats_tracker.h"

#include <algorithm>
#include <memory>
#include <set>
#include <string>
#include <utility>

#include "base/check.h"
#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/functional/function_ref.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/observer_list.h"
#include "base/power_monitor/power_monitor.h"
#include "base/scoped_multi_source_observation.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/resource_coordinator/lifecycle_unit_state.mojom.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/common/buildflags.h"
#include "chrome/common/pref_names.h"
#include "components/keep_alive_registry/keep_alive_registry.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/metrics/daily_event.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/page.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"

// We add nognchecks on some includes so that Android bots do not fail
// dependency checks.
#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/android/tab_android.h"                  // nogncheck
#include "chrome/browser/ui/android/tab_model/tab_model.h"       // nogncheck
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"  // nogncheck
#include "chrome/browser/ui/android/tab_model/tab_model_list_observer.h"  // nogncheck
#include "chrome/browser/ui/android/tab_model/tab_model_observer.h"  // nogncheck
#else
#include "chrome/browser/resource_coordinator/lifecycle_unit.h"
#include "chrome/browser/resource_coordinator/lifecycle_unit_observer.h"
#include "chrome/browser/resource_coordinator/tab_lifecycle_unit_source.h"
#include "chrome/browser/resource_coordinator/utils.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"  // nogncheck
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"  // nogncheck
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"  // nogncheck
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"  // nogncheck
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h"
#endif

namespace metrics {

namespace {

// The interval at which the DailyEvent::CheckInterval function should be
// called.
constexpr base::TimeDelta kDailyEventIntervalTimeDelta = base::Minutes(30);

// The interval at which the heartbeat tab metrics should be reported.
const base::TimeDelta kTabsHeartbeatReportingInterval = base::Minutes(5);

// The global TabStatsTracker instance.
TabStatsTracker* g_tab_stats_tracker_instance = nullptr;

void UmaHistogramCounts10000WithBatteryStateVariant(const char* histogram_name,
                                                    size_t value) {
  auto* power_monitor = base::PowerMonitor::GetInstance();
  DCHECK(power_monitor->IsInitialized());

  base::UmaHistogramCounts10000(histogram_name, value);

  const char* suffix =
      power_monitor->IsOnBatteryPower() ? ".OnBattery" : ".PluggedIn";

  base::UmaHistogramCounts10000(base::StrCat({histogram_name, suffix}), value);
}

#if !BUILDFLAG(IS_ANDROID)
void UmaHistogramCounts10000WithTabStripModeVariant(
    const char* histogram_name,
    const TabStatsTracker::TabStripInterface& tab_strip,
    size_t value) {
  // Guest mode and incognito should not count for the per-profile metrics
  if (tab_strip.GetProfile()->IsOffTheRecord()) {
    return;
  }

  auto* controller = tabs::VerticalTabStripStateController::From(
      tab_strip.browser_window_interface());
  const char* suffix = controller && controller->ShouldDisplayVerticalTabs()
                           ? ".VerticalTabStrip"
                           : ".HorizontalTabStrip";

  base::UmaHistogramCounts10000(base::StrCat({histogram_name, suffix}), value);
}
#endif

}  // namespace

TabStatsTracker::TabStripInterface::TabStripInterface(
    TabStripInterface::PlatformModel* model)
    : model_(model) {}

TabStatsTracker::TabStripInterface::~TabStripInterface() = default;

void TabStatsTracker::TabStripInterface::ForEachWebContents(
    base::FunctionRef<void(content::WebContents*)> func) const {
  for (size_t i = 0; i < GetTabCount(); ++i) {
    if (auto* web_contents = GetWebContentsAt(i)) {
      func(web_contents);
    }
  }
}

#if BUILDFLAG(IS_ANDROID)

size_t TabStatsTracker::TabStripInterface::GetTabCount() const {
  return tab_model()->GetTabCount();
}

content::WebContents* TabStatsTracker::TabStripInterface::GetActiveWebContents()
    const {
  return tab_model()->GetActiveWebContents();
}

content::WebContents* TabStatsTracker::TabStripInterface::GetWebContentsAt(
    size_t index) const {
  return tab_model()->GetWebContentsAt(index);
}

Profile* TabStatsTracker::TabStripInterface::GetProfile() {
  return tab_model()->GetProfile();
}

const Profile* TabStatsTracker::TabStripInterface::GetProfile() const {
  return tab_model()->GetProfile();
}

bool TabStatsTracker::TabStripInterface::IsInNormalBrowser() const {
  return true;
}

void TabStatsTracker::TabStripInterface::ActivateTabAtForTesting(size_t index) {
  tab_model()->SetActiveIndex(index);
}

void TabStatsTracker::TabStripInterface::CloseTabAtForTesting(size_t index) {
  tab_model()->CloseTabAt(index);
}

// static
void TabStatsTracker::TabStripInterface::ForEach(
    base::FunctionRef<void(const TabStripInterface&)> func) {
  for (TabModel* tab_model : TabModelList::models()) {
    func(TabStripInterface(tab_model));
  }
}

#else  // !BUILDFLAG(IS_ANDROID)

size_t TabStatsTracker::TabStripInterface::GetTabCount() const {
  return browser_window_interface()->GetTabStripModel()->count();
}

size_t TabStatsTracker::TabStripInterface::GetPinnedTabCount() const {
  return browser_window_interface()
      ->GetTabStripModel()
      ->IndexOfFirstNonPinnedTab();
}

#if !BUILDFLAG(IS_ANDROID)
// Returns the count of tabs within Split Views in this tab strip.
size_t TabStatsTracker::TabStripInterface::GetSplitTabCount() const {
  return browser_window_interface()->GetTabStripModel()->ListSplits().size() *
         2;
}
#endif

content::WebContents* TabStatsTracker::TabStripInterface::GetActiveWebContents()
    const {
  return browser_window_interface()->GetTabStripModel()->GetActiveWebContents();
}

content::WebContents* TabStatsTracker::TabStripInterface::GetWebContentsAt(
    size_t index) const {
  return browser_window_interface()->GetTabStripModel()->GetWebContentsAt(
      index);
}

Profile* TabStatsTracker::TabStripInterface::GetProfile() {
  return browser_window_interface()->GetProfile();
}

const Profile* TabStatsTracker::TabStripInterface::GetProfile() const {
  return browser_window_interface()->GetProfile();
}

bool TabStatsTracker::TabStripInterface::IsInNormalBrowser() const {
  return browser_window_interface()->GetType() ==
         BrowserWindowInterface::Type::TYPE_NORMAL;
}

void TabStatsTracker::TabStripInterface::ActivateTabAtForTesting(size_t index) {
  browser_window_interface()->GetTabStripModel()->ActivateTabAt(index);
}

void TabStatsTracker::TabStripInterface::CloseTabAtForTesting(size_t index) {
  browser_window_interface()->GetTabStripModel()->CloseWebContentsAt(
      index, TabCloseTypes::CLOSE_USER_GESTURE);
}

// static
void TabStatsTracker::TabStripInterface::ForEach(
    base::FunctionRef<void(const TabStripInterface&)> func) {
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&func](BrowserWindowInterface* browser) {
        func(TabStripInterface(browser));
        return true;
      });
}

#endif  // !BUILDFLAG(IS_ANDROID)

// static
const char TabStatsTracker::UmaStatsReportingDelegate::
    kNumberOfTabsOnResumeHistogramName[] = "Tabs.NumberOfTabsOnResume";
const char
    TabStatsTracker::UmaStatsReportingDelegate::kMaxTabsInADayHistogramName[] =
        "Tabs.MaxTabsInADay";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kMaxTabsPerWindowInADayHistogramName[] = "Tabs.MaxTabsPerWindowInADay";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kMaxWindowsInADayHistogramName[] = "Tabs.MaxWindowsInADay";
const char
    TabStatsTracker::UmaStatsReportingDelegate::kTabCountHistogramName[] =
        "Tabs.TabCount";
const char
    TabStatsTracker::UmaStatsReportingDelegate::kWindowCountHistogramName[] =
        "Tabs.WindowCount";
const char
    TabStatsTracker::UmaStatsReportingDelegate::kWindowWidthHistogramName[] =
        "Tabs.WindowWidth";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kVerticalTabStripCollapseStateHistogramName[] =
        "Tabs.VerticalTabs.CollapseState";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kKeyboardTabSwitchModeHistogramName[] =
        "TabStrip.Tab.KeyboardTabSwitchMode";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kFocusModeIsActiveHistogramName[] = "Tabs.FocusMode.IsActive";
const char
    TabStatsTracker::UmaStatsReportingDelegate::kPinnedTabCountHistogramName[] =
        "Tabs.PinnedTabCount";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kTabSearchIsPinnedHistogramName[] = "Tabs.TabSearch.IsPinned";

// Daily discard/reload histograms.
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyDiscardsExternalHistogramName[] = "Discarding.DailyDiscards.External";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyDiscardsUrgentHistogramName[] = "Discarding.DailyDiscards.Urgent";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyDiscardsProactiveHistogramName[] =
        "Discarding.DailyDiscards.Proactive";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyDiscardsSuggestedHistogramName[] =
        "Discarding.DailyDiscards.Suggested";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyDiscardsFrozenWithGrowingMemoryHistogramName[] =
        "Discarding.DailyDiscards.FrozenWithGrowingMemory";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyReloadsExternalHistogramName[] = "Discarding.DailyReloads.External";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyReloadsUrgentHistogramName[] = "Discarding.DailyReloads.Urgent";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyReloadsProactiveHistogramName[] = "Discarding.DailyReloads.Proactive";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyReloadsSuggestedHistogramName[] = "Discarding.DailyReloads.Suggested";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kDailyReloadsFrozenWithGrowingMemoryHistogramName[] =
        "Discarding.DailyReloads.FrozenWithGrowingMemory";

const char TabStatsTracker::UmaStatsReportingDelegate::
    kTabDuplicateCountSingleWindowHistogramName[] =
        "Tabs.Duplicates.Count.SingleWindow";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kTabDuplicateCountAllProfileWindowsHistogramName[] =
        "Tabs.Duplicates.Count.AllProfileWindows";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kTabDuplicatePercentageSingleWindowHistogramName[] =
        "Tabs.Duplicates.Percentage.SingleWindow";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kTabDuplicatePercentageAllProfileWindowsHistogramName[] =
        "Tabs.Duplicates.Percentage.AllProfileWindows";

const char TabStatsTracker::UmaStatsReportingDelegate::
    kTabDuplicateExcludingFragmentsCountSingleWindowHistogramName[] =
        "Tabs.DuplicatesExcludingFragments.Count.SingleWindow";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kTabDuplicateExcludingFragmentsCountAllProfileWindowsHistogramName[] =
        "Tabs.DuplicatesExcludingFragments.Count.AllProfileWindows";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kTabDuplicateExcludingFragmentsPercentageSingleWindowHistogramName[] =
        "Tabs.DuplicatesExcludingFragments.Percentage.SingleWindow";
const char TabStatsTracker::UmaStatsReportingDelegate::
    kTabDuplicateExcludingFragmentsPercentageAllProfileWindowsHistogramName[] =
        "Tabs.DuplicatesExcludingFragments.Percentage.AllProfileWindows";

// When initialized, TabWatcher gets the list of existing windows/tabs. There
// shouldn't be any if it's initialized at startup but this will ensure that the
// counts stay accurate if the initialization gets moved to after the creation
// of the first tab.

#if BUILDFLAG(IS_ANDROID)

class TabStatsTracker::TabWatcher final : public TabModelListObserver,
                                          public TabModelObserver,
                                          public TabAndroid::Observer {
 public:
  explicit TabWatcher(TabStatsTracker& tracker) : tracker_(tracker) {
    for (TabModel* tab_model : TabModelList::models()) {
      OnTabModelAdded(tab_model);
      for (int i = 0; i < tab_model->GetTabCount(); ++i) {
        OnTabAdded(tab_model->GetTabAt(i));
      }
      tracker_->OnTabStripNewTabCount(tab_model->GetTabCount());
    }
    TabModelList::AddObserver(this);
  }

  ~TabWatcher() final { TabModelList::RemoveObserver(this); }

  // TabModelListObserver:

  void OnTabModelAdded(TabModel* tab_model) final {
    tracker_->OnTabStripAdded();
    tab_model_observations_.AddObservation(tab_model);
  }

  void OnTabModelRemoved(TabModel* tab_model) final {
    for (int i = 0; i < tab_model->GetTabCount(); ++i) {
      if (TabAndroid* tab = tab_model->GetTabAt(i)) {
        TabRemoved(tab);
      }
    }
    tab_model_observations_.RemoveObservation(tab_model);
    tracker_->OnTabStripRemoved();
  }

  // TabModelObserver:

  void DidAddTab(TabAndroid* tab, TabModel::TabLaunchType type) final {
    OnTabAdded(tab);
    auto* tab_model = TabModelList::GetTabModelForTabAndroid(tab);
    tracker_->OnTabStripNewTabCount(CHECK_DEREF(tab_model).GetTabCount());
  }

  void TabRemoved(TabAndroid* tab) final {
    // The tab was removed from the model, either because it closed or moved to
    // a different model. Either way stop watching for the WebContents.
    if (tab_android_observations_.IsObservingSource(tab)) {
      tab_android_observations_.RemoveObservation(tab);
    }
  }

  void DidRemoveTabForClosure(TabAndroid* tab) final {
    // The tab was removed from the model, either because it closed or moved to
    // a different model. Either way stop watching for the WebContents.
    if (tab_android_observations_.IsObservingSource(tab)) {
      tab_android_observations_.RemoveObservation(tab);
    }
  }

  // TabAndroid::Observer:

  void OnInitWebContents(TabAndroid* tab) final {
    CHECK(tab->web_contents());
    tracker_->OnInitialOrInsertedTab(tab->web_contents());
    tab_android_observations_.RemoveObservation(tab);
  }

 private:
  void OnTabAdded(TabAndroid* tab) {
    if (content::WebContents* web_contents = tab->web_contents()) {
      tracker_->OnInitialOrInsertedTab(web_contents);
    } else if (!tab_android_observations_.IsObservingSource(tab)) {
      // The WebContents hasn't been attached to the tab yet. Start tracking it
      // when TabAndroid::Observer::OnInitWebContents is called. Note OnTabAdded
      // can be called while the tab is already being observed, if it's called
      // from the TabModel constructor while an async DidAddTab notification is
      // in flight.
      tab_android_observations_.AddObservation(tab);
    }
  }

  raw_ref<TabStatsTracker> tracker_;
  base::ScopedMultiSourceObservation<TabModel, TabModelObserver>
      tab_model_observations_{this};
  base::ScopedMultiSourceObservation<TabAndroid, TabAndroid::Observer>
      tab_android_observations_{this};
};

#else  // !BUILDFLAG(IS_ANDROID)

class TabStatsTracker::TabWatcher final : public BrowserCollectionObserver,
                                          public TabStripModelObserver {
 public:
  explicit TabWatcher(TabStatsTracker& tracker) : tracker_(tracker) {
    browser_collection_observation_.Observe(
        GlobalBrowserCollection::GetInstance());

    ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
        [this](BrowserWindowInterface* browser) {
          OnBrowserCreated(browser);
          TabStripModel* const tab_strip_model = browser->GetTabStripModel();
          for (int i = 0; i < tab_strip_model->count(); ++i) {
            content::WebContents* const web_contents =
                tab_strip_model->GetWebContentsAt(i);
            CHECK(web_contents);
            tracker_->OnInitialOrInsertedTab(web_contents);
          }
          tracker_->OnTabStripNewTabCount(tab_strip_model->count());
          return true;
        });
  }

  ~TabWatcher() final = default;

  // BrowserCollectionObserver:
  void OnBrowserCreated(BrowserWindowInterface* browser) final {
    tracker_->OnTabStripAdded();
    // TODO(crbug.com/452120900): TabStripModel auto-unregistered by dtor
    browser->GetTabStripModel()->AddObserver(this);
  }

  void OnBrowserClosed(BrowserWindowInterface* browser) final {
    tracker_->OnTabStripRemoved();
  }

  // TabStripModelObserver:
  void OnTabStripModelChanged(TabStripModel* tab_strip_model,
                              const TabStripModelChange& change,
                              const TabStripSelectionChange& selection) final {
    if (change.type() == TabStripModelChange::kInserted) {
      for (const auto& contents : change.GetInsert()->contents) {
        tracker_->OnInitialOrInsertedTab(contents.contents);
      }
      tracker_->OnTabStripNewTabCount(tab_strip_model->count());
    } else if (change.type() == TabStripModelChange::kReplaced) {
      auto* replace = change.GetReplace();
      tracker_->OnTabReplaced(replace->old_contents, replace->new_contents);
    }
  }

 private:
  raw_ref<TabStatsTracker> tracker_;
  base::ScopedObservation<GlobalBrowserCollection, BrowserCollectionObserver>
      browser_collection_observation_{this};
};

#endif  // !BUILDFLAG(IS_ANDROID)

const TabStatsDataStore::TabsStats& TabStatsTracker::tab_stats() const {
  return tab_stats_data_store_->tab_stats();
}

TabStatsTracker::TabStatsTracker(PrefService* pref_service)
    : reporting_delegate_(std::make_unique<UmaStatsReportingDelegate>()),
      tab_stats_data_store_(std::make_unique<TabStatsDataStore>(pref_service)),
      daily_event_(std::make_unique<DailyEvent>(
          pref_service,
          ::prefs::kTabStatsDailySample,
          // Empty to skip recording the daily event type histogram.
          /* histogram_name=*/std::string())),
      tab_watcher_(std::make_unique<TabWatcher>(*this)) {
  DCHECK(pref_service);

  AddObserverAndSetInitialState(tab_stats_data_store_.get());

  base::PowerMonitor::GetInstance()->AddPowerSuspendObserver(this);

  // Setup daily reporting of the stats aggregated in |tab_stats_data_store|.
  daily_event_->AddObserver(std::make_unique<TabStatsDailyObserver>(
      reporting_delegate_.get(), tab_stats_data_store_.get()));

  // Call the CheckInterval method to see if the data need to be immediately
  // reported.
  daily_event_->CheckInterval();
  daily_event_timer_.Start(FROM_HERE, kDailyEventIntervalTimeDelta,
                           daily_event_.get(), &DailyEvent::CheckInterval);

  heartbeat_timer_.Start(FROM_HERE, kTabsHeartbeatReportingInterval,
                         base::BindRepeating(&TabStatsTracker::OnHeartbeatEvent,
                                             base::Unretained(this)));

#if !BUILDFLAG(IS_ANDROID)
  // TODO(crbug.com/412634171): Enable this when discarding is supported on
  // Android.
  resource_coordinator::GetTabLifecycleUnitSource()->AddLifecycleObserver(this);
#endif
}

TabStatsTracker::~TabStatsTracker() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::PowerMonitor::GetInstance()->RemovePowerSuspendObserver(this);
#if !BUILDFLAG(IS_ANDROID)
  resource_coordinator::GetTabLifecycleUnitSource()->RemoveLifecycleObserver(
      this);
#endif
}

// static
void TabStatsTracker::SetInstance(std::unique_ptr<TabStatsTracker> instance) {
  CHECK(!g_tab_stats_tracker_instance);
  g_tab_stats_tracker_instance = instance.release();
}

// static
void TabStatsTracker::ClearInstance() {
  CHECK(g_tab_stats_tracker_instance);
  delete g_tab_stats_tracker_instance;
  g_tab_stats_tracker_instance = nullptr;
}

// static
TabStatsTracker* TabStatsTracker::GetInstance() {
  CHECK(g_tab_stats_tracker_instance);
  return g_tab_stats_tracker_instance;
}

// static
bool TabStatsTracker::HasInstance() {
  return g_tab_stats_tracker_instance != nullptr;
}

void TabStatsTracker::AddObserverAndSetInitialState(
    TabStatsObserver* observer) {
  tab_stats_observers_.AddObserver(observer);

  // Initialization of |this| is complete at this point and all existing
  // Browsers are already observed. TabStatsObserver functions are called
  // directly only for |observer| which is new and needs to be caught up to the
  // current state.
  TabStripInterface::ForEach([observer](const TabStripInterface& tab_strip) {
    observer->OnWindowAdded();
    tab_strip.ForEachWebContents([observer](content::WebContents* wc) {
      observer->OnTabAdded(wc);
      if (wc->GetCurrentlyPlayingVideoCount()) {
        observer->OnVideoStartedPlaying(wc);
      }
      if (wc->IsCurrentlyAudible()) {
        observer->OnTabIsAudibleChanged(wc);
      }
      if (wc->HasActiveEffectivelyFullscreenVideo()) {
        observer->OnMediaEffectivelyFullscreenChanged(wc, true);
      }
    });
  });
}

void TabStatsTracker::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(::prefs::kTabStatsTotalTabCountMax, 0);
  registry->RegisterIntegerPref(::prefs::kTabStatsMaxTabsPerWindow, 0);
  registry->RegisterIntegerPref(::prefs::kTabStatsWindowCountMax, 0);
  DailyEvent::RegisterPref(registry, ::prefs::kTabStatsDailySample);

  // Preferences for saving discard/reload counts.
  registry->RegisterIntegerPref(::prefs::kTabStatsDiscardsExternal, 0);
  registry->RegisterIntegerPref(::prefs::kTabStatsDiscardsUrgent, 0);
  registry->RegisterIntegerPref(::prefs::kTabStatsDiscardsProactive, 0);
  registry->RegisterIntegerPref(::prefs::kTabStatsDiscardsSuggested, 0);
  registry->RegisterIntegerPref(
      ::prefs::kTabStatsDiscardsFrozenWithGrowingMemory, 0);
  registry->RegisterIntegerPref(::prefs::kTabStatsReloadsExternal, 0);
  registry->RegisterIntegerPref(::prefs::kTabStatsReloadsUrgent, 0);
  registry->RegisterIntegerPref(::prefs::kTabStatsReloadsProactive, 0);
  registry->RegisterIntegerPref(::prefs::kTabStatsReloadsSuggested, 0);
  registry->RegisterIntegerPref(
      ::prefs::kTabStatsReloadsFrozenWithGrowingMemory, 0);
}

void TabStatsTracker::TabStatsDailyObserver::OnDailyEvent(
    DailyEvent::IntervalType type) {
  reporting_delegate_->ReportDailyMetrics(data_store_->tab_stats());
  data_store_->ResetMaximumsToCurrentState();
  data_store_->ClearTabDiscardAndReloadCounts();
}

class TabStatsTracker::WebContentsUsageObserver
    : public content::WebContentsObserver {
 public:
  WebContentsUsageObserver(content::WebContents* web_contents,
                           TabStatsTracker* tab_stats_tracker)
      : content::WebContentsObserver(web_contents),
        tab_stats_tracker_(tab_stats_tracker),
        ukm_source_id_(
            web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId()),
        was_playing_video_(web_contents->GetCurrentlyPlayingVideoCount() > 0) {}

  WebContentsUsageObserver(const WebContentsUsageObserver&) = delete;
  WebContentsUsageObserver& operator=(const WebContentsUsageObserver&) = delete;

  // content::WebContentsObserver:
  void DidStartNavigation(
      content::NavigationHandle* navigation_handle) override {
    // Treat browser-initiated navigations as user interactions.
    if (!navigation_handle->IsRendererInitiated()) {
      for (TabStatsObserver& tab_stats_observer :
           tab_stats_tracker_->tab_stats_observers_) {
        tab_stats_observer.OnTabInteraction(web_contents());
      }
    }
    // Update navigation time for UKM reporting.
    navigation_time_ = navigation_handle->NavigationStart();
  }

  void PrimaryPageChanged(content::Page& page) override {
    ukm_source_id_ = page.GetMainDocument().GetPageUkmSourceId();

    // Update observers.
    for (TabStatsObserver& tab_stats_observer :
         tab_stats_tracker_->tab_stats_observers_) {
      tab_stats_observer.OnPrimaryMainFrameNavigationCommitted(web_contents());
    }
  }

  void DidGetUserInteraction(const blink::WebInputEvent& event) override {
    for (TabStatsObserver& tab_stats_observer :
         tab_stats_tracker_->tab_stats_observers_) {
      tab_stats_observer.OnTabInteraction(web_contents());
    }
  }

  void OnVisibilityChanged(content::Visibility visibility) override {
    for (TabStatsObserver& tab_stats_observer :
         tab_stats_tracker_->tab_stats_observers_) {
      tab_stats_observer.OnTabVisibilityChanged(web_contents());
    }
  }

  void WebContentsDestroyed() override {
    if (ukm_source_id_) {
      ukm::builders::TabManager_TabLifetime(ukm_source_id_)
          .SetTimeSinceNavigation(
              (base::TimeTicks::Now() - navigation_time_).InMilliseconds())
          .Record(ukm::UkmRecorder::Get());
    }

    tab_stats_tracker_->OnWebContentsDestroyed(web_contents());
    // The call above will free |this| and so nothing should be done on this
    // object starting from here.
  }

  void OnAudioStateChanged(bool audible) override {
    for (TabStatsObserver& tab_stats_observer :
         tab_stats_tracker_->tab_stats_observers_) {
      tab_stats_observer.OnTabIsAudibleChanged(web_contents());
    }
  }

  void MediaEffectivelyFullscreenChanged(bool is_fullscreen) override {
    for (TabStatsObserver& tab_stats_observer :
         tab_stats_tracker_->tab_stats_observers_) {
      tab_stats_observer.OnMediaEffectivelyFullscreenChanged(web_contents(),
                                                             is_fullscreen);
    }
  }

  void MediaStartedPlaying(
      const content::WebContentsObserver::MediaPlayerInfo& media_type,
      const content::MediaPlayerId& id) override {
    MaybeNotifyVideoStartedStoppedPlaying();
  }

  void MediaStoppedPlaying(
      const content::WebContentsObserver::MediaPlayerInfo& media_type,
      const content::MediaPlayerId& id,
      content::WebContentsObserver::MediaStoppedReason reason) override {
    MaybeNotifyVideoStartedStoppedPlaying();
  }

  void MediaMetadataChanged(
      const content::WebContentsObserver::MediaPlayerInfo& video_type,
      const content::MediaPlayerId& id) override {
    MaybeNotifyVideoStartedStoppedPlaying();
  }

  void MediaDestroyed(const content::MediaPlayerId& id) override {
    for (auto& tab_stats_observer : tab_stats_tracker_->tab_stats_observers_)
      tab_stats_observer.OnMediaDestroyed(web_contents());
  }

  void WasDiscarded() override {
    if (ukm_source_id_) {
      ukm::builders::TabManager_TabLifetime(ukm_source_id_)
          .SetTimeSinceNavigation(
              (base::TimeTicks::Now() - navigation_time_).InMilliseconds())
          .Record(ukm::UkmRecorder::Get());
      ukm_source_id_ = 0;
    }

    for (TabStatsObserver& tab_stats_observer :
         tab_stats_tracker_->tab_stats_observers_) {
      tab_stats_observer.OnTabDiscarded(web_contents());
    }
  }

 private:
  void MaybeNotifyVideoStartedStoppedPlaying() {
    const bool is_playing_video =
        web_contents()->GetCurrentlyPlayingVideoCount() > 0;

    if (!was_playing_video_ && is_playing_video) {
      for (TabStatsObserver& tab_stats_observer :
           tab_stats_tracker_->tab_stats_observers_) {
        tab_stats_observer.OnVideoStartedPlaying(web_contents());
      }
    } else if (was_playing_video_ && !is_playing_video) {
      for (TabStatsObserver& tab_stats_observer :
           tab_stats_tracker_->tab_stats_observers_) {
        tab_stats_observer.OnVideoStoppedPlaying(web_contents());
      }
    }

    was_playing_video_ = is_playing_video;
  }

  raw_ptr<TabStatsTracker> tab_stats_tracker_;
  // The last navigation time associated with this tab.
  base::TimeTicks navigation_time_ = base::TimeTicks::Now();
  // Updated when a navigation is finished.
  ukm::SourceId ukm_source_id_ = 0;
  // Whether video was playing in this tab the last time we checked.
  bool was_playing_video_;
};

content::WebContentsObserver*
TabStatsTracker::GetWebContentsUsageObserverForTesting(
    content::WebContents* web_contents) {
  if (auto it = web_contents_usage_observers_.find(web_contents);
      it != web_contents_usage_observers_.end()) {
    return it->second.get();
  }
  return nullptr;
}

void TabStatsTracker::OnResume() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  reporting_delegate_->ReportTabCountOnResume(
      tab_stats_data_store_->tab_stats().total_tab_count);
}

#if !BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/412634171): Enable this when discarding is supported on
// Android.
void TabStatsTracker::OnLifecycleUnitStateChanged(
    resource_coordinator::LifecycleUnit* lifecycle_unit,
    ::mojom::LifecycleUnitState previous_state) {
  const ::mojom::LifecycleUnitState new_state = lifecycle_unit->GetState();
  if (previous_state == ::mojom::LifecycleUnitState::DISCARDED ||
      new_state == ::mojom::LifecycleUnitState::DISCARDED) {
    tab_stats_data_store_->OnTabDiscardStateChange(
        lifecycle_unit->GetDiscardReason(),
        new_state == ::mojom::LifecycleUnitState::DISCARDED);
  }
}
#endif  // !BUILDFLAG(IS_ANDROID)

void TabStatsTracker::OnTabStripAdded() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) {
    tab_stats_observer.OnWindowAdded();
  }
}

void TabStatsTracker::OnTabStripRemoved() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) {
    tab_stats_observer.OnWindowRemoved();
  }
}

void TabStatsTracker::OnTabStripNewTabCount(size_t new_tab_count) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  tab_stats_data_store_->UpdateMaxTabsPerWindowIfNeeded(new_tab_count);
}

void TabStatsTracker::OnInitialOrInsertedTab(
    content::WebContents* web_contents) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // If we already have a WebContentsObserver for this tab then it means that
  // it's already tracked and it's being dragged into a new window, there's
  // nothing to do here.
  if (!web_contents_usage_observers_.contains(web_contents)) {
    for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) {
      tab_stats_observer.OnTabAdded(web_contents);
    }
    web_contents_usage_observers_.insert(std::make_pair(
        web_contents,
        std::make_unique<WebContentsUsageObserver>(web_contents, this)));
  }
}

void TabStatsTracker::OnTabReplaced(content::WebContents* old_contents,
                                    content::WebContents* new_contents) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) {
    tab_stats_observer.OnTabReplaced(old_contents, new_contents);
  }
  web_contents_usage_observers_.insert(std::make_pair(
      new_contents,
      std::make_unique<WebContentsUsageObserver>(new_contents, this)));
  web_contents_usage_observers_.erase(old_contents);
}

void TabStatsTracker::OnWebContentsDestroyed(
    content::WebContents* web_contents) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(web_contents_usage_observers_.contains(web_contents));
  web_contents_usage_observers_.erase(
      web_contents_usage_observers_.find(web_contents));
  for (TabStatsObserver& tab_stats_observer : tab_stats_observers_) {
    tab_stats_observer.OnTabRemoved(web_contents);
  }
}

void TabStatsTracker::OnHeartbeatEvent() {
  reporting_delegate_->ReportHeartbeatMetrics(
      tab_stats_data_store_->tab_stats());
}

void TabStatsTracker::UmaStatsReportingDelegate::ReportTabCountOnResume(
    size_t tab_count) {
  // Don't report the number of tabs on resume if Chrome is running in
  // background with no visible window.
  if (IsChromeBackgroundedWithoutWindows())
    return;
  UmaHistogramCounts10000WithBatteryStateVariant(
      kNumberOfTabsOnResumeHistogramName, tab_count);
}

void TabStatsTracker::UmaStatsReportingDelegate::ReportDailyMetrics(
    const TabStatsDataStore::TabsStats& tab_stats) {
  // Don't report the counts if they're equal to 0, this means that Chrome has
  // only been running in the background since the last time the metrics have
  // been reported.
  if (tab_stats.total_tab_count_max == 0)
    return;
  UmaHistogramCounts10000WithBatteryStateVariant(kMaxTabsInADayHistogramName,
                                                 tab_stats.total_tab_count_max);
  UmaHistogramCounts10000WithBatteryStateVariant(
      kMaxTabsPerWindowInADayHistogramName, tab_stats.max_tab_per_window);
  UmaHistogramCounts10000WithBatteryStateVariant(kMaxWindowsInADayHistogramName,
                                                 tab_stats.window_count_max);

  // Reports the discard/reload counts.
  const size_t external_index =
      static_cast<size_t>(LifecycleUnitDiscardReason::EXTERNAL);
  const size_t urgent_index =
      static_cast<size_t>(LifecycleUnitDiscardReason::URGENT);
  const size_t proactive_index =
      static_cast<size_t>(LifecycleUnitDiscardReason::PROACTIVE);
  const size_t suggested_index =
      static_cast<size_t>(LifecycleUnitDiscardReason::SUGGESTED);
  const size_t frozen_with_growing_memory_index = static_cast<size_t>(
      LifecycleUnitDiscardReason::FROZEN_WITH_GROWING_MEMORY);
  base::UmaHistogramCounts10000(kDailyDiscardsExternalHistogramName,
                                tab_stats.tab_discard_counts[external_index]);
  base::UmaHistogramCounts10000(kDailyDiscardsUrgentHistogramName,
                                tab_stats.tab_discard_counts[urgent_index]);
  base::UmaHistogramCounts10000(kDailyDiscardsProactiveHistogramName,
                                tab_stats.tab_discard_counts[proactive_index]);
  base::UmaHistogramCounts10000(kDailyDiscardsSuggestedHistogramName,
                                tab_stats.tab_discard_counts[suggested_index]);
  base::UmaHistogramCounts10000(
      kDailyDiscardsFrozenWithGrowingMemoryHistogramName,
      tab_stats.tab_discard_counts[frozen_with_growing_memory_index]);
  base::UmaHistogramCounts10000(kDailyReloadsExternalHistogramName,
                                tab_stats.tab_reload_counts[external_index]);
  base::UmaHistogramCounts10000(kDailyReloadsUrgentHistogramName,
                                tab_stats.tab_reload_counts[urgent_index]);
  base::UmaHistogramCounts10000(kDailyReloadsProactiveHistogramName,
                                tab_stats.tab_reload_counts[proactive_index]);
  base::UmaHistogramCounts10000(kDailyReloadsSuggestedHistogramName,
                                tab_stats.tab_reload_counts[suggested_index]);
  base::UmaHistogramCounts10000(
      kDailyReloadsFrozenWithGrowingMemoryHistogramName,
      tab_stats.tab_reload_counts[frozen_with_growing_memory_index]);

#if !BUILDFLAG(IS_ANDROID)
  std::set<const Profile*> profiles;
  TabStripInterface::ForEach([&profiles](const TabStripInterface& tab_strip) {
    profiles.insert(tab_strip.GetProfile());
  });

  for (const Profile* profile : profiles) {
    if (profile->IsOffTheRecord()) {
      continue;
    }
    // Record the keyboard tab switch mode for each profile.
    bool mru_enabled = profile->GetPrefs()->GetBoolean(prefs::kCtrlTabMru);
    base::UmaHistogramEnumeration(kKeyboardTabSwitchModeHistogramName,
                                  mru_enabled
                                      ? KeyboardTabSwitchMode::kMRU
                                      : KeyboardTabSwitchMode::kStandard);

    // Record tab search pinned state for each profile.
    bool is_tab_search_pinned =
        profile->GetPrefs()->GetBoolean(prefs::kTabSearchPinnedToTabstrip);
    base::UmaHistogramBoolean(kTabSearchIsPinnedHistogramName,
                              is_tab_search_pinned);
  }
#endif
}

void TabStatsTracker::UmaStatsReportingDelegate::ReportHeartbeatMetrics(
    const TabStatsDataStore::TabsStats& tab_stats) {
  // Don't report anything if Chrome is running in background with no visible
  // window.
  if (IsChromeBackgroundedWithoutWindows()) {
    return;
  }

  UmaHistogramCounts10000WithBatteryStateVariant(kTabCountHistogramName,
                                                 tab_stats.total_tab_count);
  UmaHistogramCounts10000WithBatteryStateVariant(kWindowCountHistogramName,
                                                 tab_stats.window_count);

  if (base::FeatureList::IsEnabled(features::kTabDuplicateMetrics)) {
    ReportTabDuplicateMetrics(true);
    ReportTabDuplicateMetrics(false);
  }

#if !BUILDFLAG(IS_ANDROID)
  ReportSplitTabMetrics();

  TabStripInterface::ForEach([&](const TabStripInterface& tab_strip) {
    if (!tab_strip.IsInNormalBrowser()) {
      return;
    }

    UmaHistogramCounts10000WithTabStripModeVariant(
        kTabCountHistogramName, tab_strip, tab_strip.GetTabCount());
    UmaHistogramCounts10000WithTabStripModeVariant(
        kPinnedTabCountHistogramName, tab_strip, tab_strip.GetPinnedTabCount());

    const TabStripModel* tab_strip_model =
        tab_strip.browser_window_interface()->GetTabStripModel();
    const bool is_in_focus_mode =
        tab_strip_model && tab_strip_model->GetFocusedGroup().has_value();
    base::UmaHistogramBoolean(kFocusModeIsActiveHistogramName,
                              is_in_focus_mode);

    auto* controller = tabs::VerticalTabStripStateController::From(
        tab_strip.browser_window_interface());
    if (controller && controller->ShouldDisplayVerticalTabs()) {
      base::UmaHistogramEnumeration(
          kVerticalTabStripCollapseStateHistogramName,
          controller->IsCollapsed() ? VerticalTabStripCollapseState::kCollapsed
                                    : VerticalTabStripCollapseState::kExpanded);
    }

    // Record the width of all open browser windows with tabs.
    const ui::BaseWindow* window =
        tab_strip.browser_window_interface()->GetWindow();

    // Only consider visible windows.
    if (!window->IsVisible() || window->IsMinimized()) {
      return;
    }

    // Get the window's size (in DIPs).
    const gfx::Size window_size = window->GetBounds().size();

    // If the size is for some reason 0 in either dimension, skip it.
    if (window_size.IsEmpty()) {
      return;
    }

    // A 4K screen is 4096 pixels wide. Doubling this and rounding up to
    // 10000 should give a reasonable upper bound on DIPs. For the
    // minimum width, pick an arbitrary value of 100. Most screens are
    // unlikely to be this small, and likewise a browser window's min
    // width is around this size.
    UMA_HISTOGRAM_CUSTOM_COUNTS(kWindowWidthHistogramName, window_size.width(),
                                100, 10000, 50);
  });
#endif
}

void TabStatsTracker::UmaStatsReportingDelegate::ReportTabDuplicateMetrics(
    bool exclude_fragments) {
  std::map<const Profile*, DuplicateData> duplicate_data_per_profile;
  TabStripInterface::ForEach([&](const TabStripInterface& tab_strip) {
    if (!tab_strip.IsInNormalBrowser()) {
      return;
    }

    const Profile* const profile = tab_strip.GetProfile();
    DuplicateData duplicate_data_multi_window =
        duplicate_data_per_profile[profile];
    DuplicateData duplicate_data_single_window = DuplicateData();

    const size_t tab_count = tab_strip.GetTabCount();
    duplicate_data_multi_window.tab_count += tab_count;
    duplicate_data_single_window.tab_count = tab_count;

    tab_strip.ForEachWebContents([&](content::WebContents* web_contents) {
      const GURL full_url = web_contents->GetURL();
      const GURL url = exclude_fragments ? full_url.GetWithoutRef() : full_url;
      auto seen_urls_single_window_result =
          duplicate_data_single_window.seen_urls.insert(url);
      if (!seen_urls_single_window_result.second) {
        duplicate_data_single_window.duplicate_count++;
      }
      // Guest mode and incognito should not count for the per-profile metrics
      if (profile->IsOffTheRecord()) {
        return;
      }
      auto seen_urls_multi_window_result =
          duplicate_data_multi_window.seen_urls.insert(url);
      if (!seen_urls_multi_window_result.second) {
        duplicate_data_multi_window.duplicate_count++;
      }
    });
    duplicate_data_per_profile[profile] = duplicate_data_multi_window;

    base::UmaHistogramCounts100(
        exclude_fragments
            ? kTabDuplicateExcludingFragmentsCountSingleWindowHistogramName
            : kTabDuplicateCountSingleWindowHistogramName,
        duplicate_data_single_window.duplicate_count);
    if (duplicate_data_single_window.tab_count > 0) {
      base::UmaHistogramPercentage(
          exclude_fragments
              ? kTabDuplicateExcludingFragmentsPercentageSingleWindowHistogramName
              : kTabDuplicatePercentageSingleWindowHistogramName,
          duplicate_data_single_window.duplicate_count * 100 /
              duplicate_data_single_window.tab_count);
    }
  });

  for (const auto& duplicate_data : duplicate_data_per_profile) {
    // Guest mode and incognito should not count for the per-profile metrics
    const Profile* const profile = duplicate_data.first;
    if (profile->IsOffTheRecord()) {
      continue;
    }

    base::UmaHistogramCounts100(
        exclude_fragments
            ? kTabDuplicateExcludingFragmentsCountAllProfileWindowsHistogramName
            : kTabDuplicateCountAllProfileWindowsHistogramName,
        duplicate_data.second.duplicate_count);
    if (duplicate_data.second.tab_count > 0) {
      base::UmaHistogramPercentage(
          exclude_fragments
              ? kTabDuplicateExcludingFragmentsPercentageAllProfileWindowsHistogramName
              : kTabDuplicatePercentageAllProfileWindowsHistogramName,
          duplicate_data.second.duplicate_count * 100 /
              duplicate_data.second.tab_count);
    }
  }
}

#if !BUILDFLAG(IS_ANDROID)
void TabStatsTracker::UmaStatsReportingDelegate::ReportSplitTabMetrics() {
  int split_tabs = 0;
  TabStripInterface::ForEach([&](const TabStripInterface& tab_strip) {
    if (!tab_strip.IsInNormalBrowser()) {
      return;
    }

    split_tabs += tab_strip.GetSplitTabCount();
  });

  base::UmaHistogramCounts10000(
      base::StrCat({kTabCountHistogramName, ".SplitTabs"}), split_tabs);
}
#endif

bool TabStatsTracker::UmaStatsReportingDelegate::
    IsChromeBackgroundedWithoutWindows() {
#if BUILDFLAG(ENABLE_BACKGROUND_MODE)
  return KeepAliveRegistry::GetInstance()->WouldRestartWithout({
      // Transient startup related KeepAlives, not related to any UI.
      KeepAliveOrigin::SESSION_RESTORE,
      KeepAliveOrigin::BACKGROUND_MODE_MANAGER_STARTUP,

      KeepAliveOrigin::BACKGROUND_SYNC,

      // Notification KeepAlives are not dependent on the Chrome UI being
      // loaded, and can be registered when we were in pure background mode.
      // They just block it to avoid issues. Ignore them when determining if we
      // are in that mode.
      KeepAliveOrigin::NOTIFICATION,
      KeepAliveOrigin::PENDING_NOTIFICATION_CLICK_EVENT,
      KeepAliveOrigin::PENDING_NOTIFICATION_CLOSE_EVENT,
      KeepAliveOrigin::IN_FLIGHT_PUSH_MESSAGE,
  });
#else
  return false;
#endif  // BUILDFLAG(ENABLE_BACKGROUND_MODE)
}

TabStatsTracker::UmaStatsReportingDelegate::DuplicateData::DuplicateData() {
  duplicate_count = 0;
  tab_count = 0;
  seen_urls = {};
}

TabStatsTracker::UmaStatsReportingDelegate::DuplicateData::DuplicateData(
    const DuplicateData& other) {
  duplicate_count = other.duplicate_count;
  tab_count = other.tab_count;
  seen_urls = std::set(other.seen_urls);
}

TabStatsTracker::UmaStatsReportingDelegate::DuplicateData::~DuplicateData() =
    default;

}  // namespace metrics
