// Copyright 2015 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/sessions/session_restore_stats_collector.h"

#include <string>
#include <utility>

#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/task/bind_post_task.h"
#include "components/performance_manager/public/decorators/site_data_recorder.h"
#include "components/performance_manager/public/features.h"
#include "components/performance_manager/public/graph/page_node.h"
#include "components/performance_manager/public/performance_manager.h"
#include "components/performance_manager/public/persistence/site_data/feature_usage.h"
#include "components/performance_manager/public/persistence/site_data/site_data_reader.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/permission_controller.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace {

using content::RenderWidgetHost;
using content::RenderWidgetHostView;
using content::WebContents;
using performance_manager::PerformanceManager;
using performance_manager::SiteDataReader;
using performance_manager::SiteFeatureUsage;

SessionRestoreStatsCollector* g_instance = nullptr;

// Returns the RenderWidgetHost associated with a WebContents.
RenderWidgetHost* GetRenderWidgetHost(WebContents* web_contents) {
  content::RenderWidgetHostView* render_widget_host_view =
      web_contents->GetRenderWidgetHostView();
  if (render_widget_host_view)
    return render_widget_host_view->GetRenderWidgetHost();
  return nullptr;
}

bool HasNotificationPermission(content::WebContents* contents) {
  return contents->GetBrowserContext()
             ->GetPermissionController()
             ->GetPermissionResultForOriginWithoutContext(
                 content::PermissionDescriptorUtil::
                     CreatePermissionDescriptorForPermissionType(
                         blink::PermissionType::NOTIFICATIONS),
                 url::Origin::Create(contents->GetLastCommittedURL()))
             .status == blink::mojom::PermissionStatus::GRANTED;
}

void LogFirstPaintHistogram(base::TimeDelta paint_time,
                            std::string_view suffix = "") {
  base::UmaHistogramCustomTimes(
      base::StrCat({"SessionRestore.ForegroundTabFirstPaint4", suffix}),
      paint_time, base::Milliseconds(100), base::Minutes(16), 50);
}

}  // namespace

SessionRestoreStatsCollector::TabLoaderStats::TabLoaderStats()
    : tab_count(0u),
      tab_first_paint_reason(PAINT_FINISHED_UMA_MAX) {}

SessionRestoreStatsCollector::SessionRestoreStatsCollector(
    const base::TimeTicks& restore_started,
    std::unique_ptr<StatsReportingDelegate> reporting_delegate)
    : non_restored_tab_painted_first_(false),
      hidden_or_occluded_tab_ignored_(false),
      restore_started_(restore_started),
      reporting_delegate_(std::move(reporting_delegate)) {
  DCHECK(!g_instance);
  g_instance = this;
}

SessionRestoreStatsCollector::~SessionRestoreStatsCollector() {
  DCHECK_EQ(g_instance, this);
  g_instance = nullptr;
}

// static
SessionRestoreStatsCollector* SessionRestoreStatsCollector::GetOrCreateInstance(
    base::TimeTicks restore_started,
    std::unique_ptr<StatsReportingDelegate> reporting_delegate) {
  if (g_instance)
    return g_instance;
  return new SessionRestoreStatsCollector(restore_started,
                                          std::move(reporting_delegate));
}

void SessionRestoreStatsCollector::TrackTabs(
    const std::vector<SessionRestoreDelegate::RestoredTab>& tabs) {
  tab_loader_stats_.tab_count += tabs.size();
  for (const auto& tab : tabs) {
    RegisterObserverForTab(tab.contents());
    if (tab.is_active()) {
      tab_loader_stats_.active_tab_count++;
    }
    if (tab.is_app()) {
      tab_loader_stats_.app_tab_count++;
    }
    if (tab.is_internal_page()) {
      tab_loader_stats_.internal_page_tab_count++;
    }
    if (tab.is_pinned()) {
      tab_loader_stats_.pinned_tab_count++;
    }
    if (tab.group().has_value()) {
      tab_loader_stats_.grouped_tab_count++;
    }

    // Look up background feature use in `site_data_reader` on the PM sequence,
    // and post the result to OnTabUpdatesInBackground() on the current
    // sequence, along with permissions info.
    base::OnceCallback<bool(const SiteDataReader&)> on_site_data_ready =
        base::BindOnce([](const SiteDataReader& site_data_reader) {
          return site_data_reader.UpdatesFaviconInBackground() ==
                     SiteFeatureUsage::kSiteFeatureInUse ||
                 site_data_reader.UpdatesTitleInBackground() ==
                     SiteFeatureUsage::kSiteFeatureInUse;
        });

    base::OnceCallback<void(bool)> on_tab_updates_in_background =
        base::BindOnce(&SessionRestoreStatsCollector::OnTabUpdatesInBackground,
                       weak_factory_.GetWeakPtr(),
                       HasNotificationPermission(tab.contents()));

    performance_manager::WaitForSiteDataReader(
        PerformanceManager::GetPrimaryPageNodeForWebContents(tab.contents()),
        std::move(on_site_data_ready)
            .Then(std::move(on_tab_updates_in_background)));
  }

  // If we were not able to register observers for any tab, report stats.
  if (tracked_tabs_occluded_map_.empty())
    ReportStatsAndSelfDestroy();
}

void SessionRestoreStatsCollector::RenderWidgetHostVisibilityChanged(
    content::RenderWidgetHost* widget_host,
    bool became_visible) {
  if (became_visible)
    return;

  auto host_and_occluded_it = tracked_tabs_occluded_map_.find(widget_host);
  if (host_and_occluded_it == tracked_tabs_occluded_map_.end())
    return;

  host_and_occluded_it->second = true;
}

void SessionRestoreStatsCollector::RenderWidgetHostDidUpdateVisualProperties(
    content::RenderWidgetHost* widget_host) {
  // Only process this event if first paint hasn't been seen and this is a
  // paint of a tab that has not been hidden or occluded.
  if (!widget_host->GetView() || !widget_host->GetView()->IsShowing())
    return;

  auto host_and_occluded_it = tracked_tabs_occluded_map_.find(widget_host);
  if (host_and_occluded_it != tracked_tabs_occluded_map_.end()) {
    // Ignore first paint of a restored tab that was hidden or occluded
    // before first paint. If another restored tab is painted, its paint
    // time will be recorded.
    if (host_and_occluded_it->second) {
      hidden_or_occluded_tab_ignored_ = true;
      return;
    }
    // This is a paint for a tab that is explicitly being tracked so
    // update the statistics. Otherwise the host is for a tab that's not
    // being tracked. Thus some other tab has visibility and has rendered
    // and there's no point in tracking the time to first paint. This can
    // happen because the user opened a different tab or restored tabs
    // to an already existing browser and an existing tab was in the
    // foreground.
    base::TimeDelta time_to_paint = base::TimeTicks::Now() - restore_started_;
    tab_loader_stats_.foreground_tab_first_paint = time_to_paint;
  } else {
    non_restored_tab_painted_first_ = true;
  }

  ReportStatsAndSelfDestroy();
}

void SessionRestoreStatsCollector::RenderWidgetHostDestroyed(
    content::RenderWidgetHost* widget_host) {
  render_widget_host_observations_.RemoveObservation(widget_host);
  tracked_tabs_occluded_map_.erase(widget_host);

  if (tracked_tabs_occluded_map_.empty())
    ReportStatsAndSelfDestroy();
}

void SessionRestoreStatsCollector::RegisterObserverForTab(WebContents* tab) {
  content::RenderWidgetHost* rwh = GetRenderWidgetHost(tab);

  // If we don't have a RenderWidgetHost, we can't track paints for the tab so
  // ignore it.
  if (!rwh)
    return;

  // Assume that tabs do not start out hidden or occluded.
  auto result = tracked_tabs_occluded_map_.emplace(rwh, false);
  DCHECK(result.second);
  render_widget_host_observations_.AddObservation(rwh);
}

void SessionRestoreStatsCollector::ReportStatsAndSelfDestroy() {
  if (!tab_loader_stats_.foreground_tab_first_paint.is_zero()) {
    tab_loader_stats_.tab_first_paint_reason = PAINT_FINISHED_UMA_DONE;
  } else if (non_restored_tab_painted_first_) {
    tab_loader_stats_.tab_first_paint_reason =
        PAINT_FINISHED_NON_RESTORED_TAB_PAINTED_FIRST;
  } else if (hidden_or_occluded_tab_ignored_) {
    tab_loader_stats_.tab_first_paint_reason =
        PAINT_FINISHED_UMA_NO_COMPLETELY_VISIBLE_TABS;
  } else {
    tab_loader_stats_.tab_first_paint_reason = PAINT_FINISHED_UMA_NO_PAINT;
  }
  reporting_delegate_->ReportTabLoaderStats(tab_loader_stats_);

  delete this;
}

void SessionRestoreStatsCollector::OnTabUpdatesInBackground(
    bool background_notification_permission,
    bool updates_in_background) {
  if (background_notification_permission) {
    tab_loader_stats_.notification_permission_tab_count++;
  }
  if (updates_in_background) {
    tab_loader_stats_.updates_in_background_tab_count++;
  }
}

SessionRestoreStatsCollector::UmaStatsReportingDelegate::
    UmaStatsReportingDelegate() = default;

void SessionRestoreStatsCollector::UmaStatsReportingDelegate::
    ReportTabLoaderStats(const TabLoaderStats& tab_loader_stats) {
  if (!tab_loader_stats.foreground_tab_first_paint.is_zero()) {
    LogFirstPaintHistogram(tab_loader_stats.foreground_tab_first_paint);
    std::string_view count_suffix;
    CHECK_GT(tab_loader_stats.tab_count, 0u);
    if (tab_loader_stats.tab_count < 2) {
      count_suffix = ".1Tab";
    } else if (tab_loader_stats.tab_count < 4) {
      count_suffix = ".2to3Tabs";
    } else if (tab_loader_stats.tab_count < 8) {
      count_suffix = ".4to7Tabs";
    } else if (tab_loader_stats.tab_count < 16) {
      count_suffix = ".8to15Tabs";
    } else if (tab_loader_stats.tab_count < 32) {
      count_suffix = ".16to31Tabs";
    } else {
      count_suffix = ".32PlusTabs";
    }
    LogFirstPaintHistogram(tab_loader_stats.foreground_tab_first_paint,
                           count_suffix);
  }
  base::UmaHistogramEnumeration(
      "SessionRestore.ForegroundTabFirstPaint4.FinishReason",
      tab_loader_stats.tab_first_paint_reason, PAINT_FINISHED_UMA_MAX);

  base::UmaHistogramCounts100("SessionRestore.TabCount",
                              tab_loader_stats.tab_count);
  base::UmaHistogramCounts100("SessionRestore.TabCount.Active",
                              tab_loader_stats.active_tab_count);
  base::UmaHistogramCounts100("SessionRestore.TabCount.App",
                              tab_loader_stats.app_tab_count);
  base::UmaHistogramCounts100("SessionRestore.TabCount.InternalPage",
                              tab_loader_stats.internal_page_tab_count);
  base::UmaHistogramCounts100("SessionRestore.TabCount.Pinned",
                              tab_loader_stats.pinned_tab_count);
  base::UmaHistogramCounts100("SessionRestore.TabCount.Grouped",
                              tab_loader_stats.grouped_tab_count);
  base::UmaHistogramCounts100(
      "SessionRestore.TabCount.BackgroundNotificationPermission",
      tab_loader_stats.notification_permission_tab_count);
  base::UmaHistogramCounts100(
      "SessionRestore.TabCount.UpdatesTitleOrFaviconInBackground",
      tab_loader_stats.updates_in_background_tab_count);
}
