// Copyright 2024 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/performance_manager/scenarios/loading_scenario_observer.h"

#include <atomic>
#include <vector>

#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/notreached.h"
#include "base/numerics/checked_math.h"
#include "base/sequence_checker.h"
#include "components/performance_manager/graph/graph_impl.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/graph/process_node_impl.h"
#include "components/performance_manager/scenario_api/performance_scenarios.h"
#include "components/performance_manager/scenarios/browser_performance_scenarios.h"
#include "components/performance_manager/scenarios/loading_scenario_data.h"

namespace performance_manager {

namespace {

bool StateIsLoading(PageNode::LoadingState loading_state) {
  switch (loading_state) {
    case PageNode::LoadingState::kLoadingNotStarted:
    case PageNode::LoadingState::kLoadingTimedOut:
    case PageNode::LoadingState::kLoadedIdle:
      return false;
    case PageNode::LoadingState::kLoading:
    case PageNode::LoadingState::kLoadedBusy:
      return true;
  }
  NOTREACHED();
}

LoadingScenario CalculateLoadingScenario(const LoadingScenarioCounts& counts) {
  if (counts.focused_loading_pages() > 0) {
    return LoadingScenario::kFocusedPageLoading;
  }
  if (counts.visible_loading_pages() > 0) {
    return LoadingScenario::kVisiblePageLoading;
  }
  if (counts.loading_pages() > 0) {
    return LoadingScenario::kBackgroundPageLoading;
  }
  return LoadingScenario::kNoPageLoading;
}

}  // namespace

LoadingScenarioObserver::LoadingScenarioObserver() = default;

LoadingScenarioObserver::~LoadingScenarioObserver() = default;

void LoadingScenarioObserver::OnFrameNodeAdded(const FrameNode* frame_node) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  const ProcessNode* process_node = frame_node->GetProcessNode();
  const PageNode* page_node = frame_node->GetPageNode();
  const size_t new_frame_count =
      LoadingScenarioPageFrameCounts::Get(PageNodeImpl::FromNode(page_node))
          .IncrementFrameCountForProcess(process_node);
  if (new_frame_count == 1 && StateIsLoading(page_node->GetLoadingState())) {
    // Process joined a loading page. Need to update process state.
    auto& loading_counts =
        LoadingScenarioCounts::Get(ProcessNodeImpl::FromNode(process_node));
    loading_counts.IncrementLoadingPageCounts(page_node->IsVisible(),
                                              page_node->IsFocused());
    SetLoadingScenarioForProcessNode(CalculateLoadingScenario(loading_counts),
                                     process_node);
  }
}

void LoadingScenarioObserver::OnBeforeFrameNodeRemoved(
    const FrameNode* frame_node) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  const ProcessNode* process_node = frame_node->GetProcessNode();
  const PageNode* page_node = frame_node->GetPageNode();
  const size_t new_frame_count =
      LoadingScenarioPageFrameCounts::Get(PageNodeImpl::FromNode(page_node))
          .DecrementFrameCountForProcess(process_node);
  if (new_frame_count == 0 && StateIsLoading(page_node->GetLoadingState())) {
    // Process no longer part of a loading page. Need to update process state.
    auto& loading_counts =
        LoadingScenarioCounts::Get(ProcessNodeImpl::FromNode(process_node));
    loading_counts.DecrementLoadingPageCounts(page_node->IsVisible(),
                                              page_node->IsFocused());
    SetLoadingScenarioForProcessNode(CalculateLoadingScenario(loading_counts),
                                     process_node);
  }
}

void LoadingScenarioObserver::OnPageNodeAdded(const PageNode* page_node) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(page_node->GetMainFrameNodes().empty());
  LoadingScenarioPageFrameCounts::Create(PageNodeImpl::FromNode(page_node));
  if (StateIsLoading(page_node->GetLoadingState())) {
    auto process_nodes =
        LoadingScenarioPageFrameCounts::Get(PageNodeImpl::FromNode(page_node))
            .GetProcessesWithFramesInPage();
    IncrementLoadingCounts(process_nodes, page_node->IsVisible(),
                           page_node->IsFocused());
    UpdateLoadingScenarios(process_nodes);
  }
}

void LoadingScenarioObserver::OnBeforePageNodeRemoved(
    const PageNode* page_node) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (StateIsLoading(page_node->GetLoadingState())) {
    auto process_nodes =
        LoadingScenarioPageFrameCounts::Get(PageNodeImpl::FromNode(page_node))
            .GetProcessesWithFramesInPage();
    DecrementLoadingCounts(process_nodes, page_node->IsVisible(),
                           page_node->IsFocused());
    UpdateLoadingScenarios(process_nodes);
  }
}

void LoadingScenarioObserver::OnIsFocusedChanged(const PageNode* page_node) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (StateIsLoading(page_node->GetLoadingState())) {
    auto process_nodes =
        LoadingScenarioPageFrameCounts::Get(PageNodeImpl::FromNode(page_node))
            .GetProcessesWithFramesInPage();
    DecrementLoadingCounts(process_nodes, page_node->IsVisible(),
                           !page_node->IsFocused());
    IncrementLoadingCounts(process_nodes, page_node->IsVisible(),
                           page_node->IsFocused());
    UpdateLoadingScenarios(process_nodes);
  }
}

void LoadingScenarioObserver::OnIsVisibleChanged(const PageNode* page_node) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (StateIsLoading(page_node->GetLoadingState())) {
    auto process_nodes =
        LoadingScenarioPageFrameCounts::Get(PageNodeImpl::FromNode(page_node))
            .GetProcessesWithFramesInPage();
    DecrementLoadingCounts(process_nodes, !page_node->IsVisible(),
                           page_node->IsFocused());
    IncrementLoadingCounts(process_nodes, page_node->IsVisible(),
                           page_node->IsFocused());
    UpdateLoadingScenarios(process_nodes);
  }
}

void LoadingScenarioObserver::OnLoadingStateChanged(
    const PageNode* page_node,
    PageNode::LoadingState previous_state) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  const bool is_loading = StateIsLoading(page_node->GetLoadingState());
  const bool was_loading = StateIsLoading(previous_state);
  if (is_loading != was_loading) {
    auto process_nodes =
        LoadingScenarioPageFrameCounts::Get(PageNodeImpl::FromNode(page_node))
            .GetProcessesWithFramesInPage();
    if (is_loading) {
      IncrementLoadingCounts(process_nodes, page_node->IsVisible(),
                             page_node->IsFocused());
    } else {
      DecrementLoadingCounts(process_nodes, page_node->IsVisible(),
                             page_node->IsFocused());
    }
    UpdateLoadingScenarios(process_nodes);
  }
}

void LoadingScenarioObserver::OnProcessNodeAdded(
    const ProcessNode* process_node) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  LoadingScenarioCounts::Create(ProcessNodeImpl::FromNode(process_node));
}

void LoadingScenarioObserver::OnPassedToGraph(Graph* graph) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Must be created before any nodes are added. This is a simplification to
  // avoid extra code to calculating the current scenario here.
  CHECK(graph->GetAllPageNodes().empty());
  CHECK(graph->GetAllProcessNodes().empty());
  CHECK(graph->GetAllFrameNodes().empty());
  CHECK_EQ(performance_scenarios::GetLoadingScenario(
               performance_scenarios::ScenarioScope::kGlobal)
               ->load(std::memory_order_relaxed),
           LoadingScenario::kNoPageLoading);
  graph->AddFrameNodeObserver(this);
  graph->AddPageNodeObserver(this);
  graph->AddProcessNodeObserver(this);
}

void LoadingScenarioObserver::OnTakenFromGraph(Graph* graph) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  graph->RemoveFrameNodeObserver(this);
  graph->RemovePageNodeObserver(this);
  graph->RemoveProcessNodeObserver(this);
  SetGlobalLoadingScenario(LoadingScenario::kNoPageLoading);
  for (const ProcessNode* process_node : graph->GetAllProcessNodes()) {
    SetLoadingScenarioForProcessNode(LoadingScenario::kNoPageLoading,
                                     process_node);
  }
}

void LoadingScenarioObserver::IncrementLoadingCounts(
    base::span<const ProcessNode*> process_nodes,
    bool is_visible,
    bool is_focused) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  global_counts_.IncrementLoadingPageCounts(is_visible, is_focused);
  for (const ProcessNode* process_node : process_nodes) {
    LoadingScenarioCounts::Get(ProcessNodeImpl::FromNode(process_node))
        .IncrementLoadingPageCounts(is_visible, is_focused);
  }
}

void LoadingScenarioObserver::DecrementLoadingCounts(
    base::span<const ProcessNode*> process_nodes,
    bool is_visible,
    bool is_focused) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  global_counts_.DecrementLoadingPageCounts(is_visible, is_focused);
  for (const ProcessNode* process_node : process_nodes) {
    LoadingScenarioCounts::Get(ProcessNodeImpl::FromNode(process_node))
        .DecrementLoadingPageCounts(is_visible, is_focused);
  }
}

void LoadingScenarioObserver::UpdateLoadingScenarios(
    base::span<const ProcessNode*> process_nodes) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  SetGlobalLoadingScenario(CalculateLoadingScenario(global_counts_));
  for (const ProcessNode* process_node : process_nodes) {
    SetLoadingScenarioForProcessNode(
        CalculateLoadingScenario(LoadingScenarioCounts::Get(
            ProcessNodeImpl::FromNode(process_node))),
        process_node);
  }
}

}  // namespace performance_manager
