// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROME_BROWSER_PERFORMANCE_MANAGER_POLICIES_DISCARD_ELIGIBILITY_POLICY_H_
#define CHROME_BROWSER_PERFORMANCE_MANAGER_POLICIES_DISCARD_ELIGIBILITY_POLICY_H_

#include <map>

#include "base/containers/span.h"
#include "base/functional/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "chrome/browser/performance_manager/policies/cannot_discard_reason.h"
#include "chrome/browser/resource_coordinator/lifecycle_unit_state.mojom-shared.h"
#include "components/performance_manager/public/graph/graph.h"
#include "components/performance_manager/public/graph/graph_registered.h"
#include "components/performance_manager/public/graph/node_data_describer.h"
#include "components/performance_manager/public/graph/page_node.h"

namespace url_matcher {
class URLMatcher;
}  // namespace url_matcher

namespace performance_manager::policies {

namespace internal {
// kNonVisiblePagesUrgentProtectionTime is encapsulated in CanDiscard(). This is
// only accessible to testing code.
#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_ANDROID)
inline constexpr base::TimeDelta kNonVisiblePagesUrgentProtectionTime =
    base::TimeDelta();
#else
// Time during which non visible pages are protected from urgent discarding
// (not on ChromeOS).
inline constexpr base::TimeDelta kNonVisiblePagesUrgentProtectionTime =
    base::Minutes(10);
#endif

}  // namespace internal

#if BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/412839833): kTabAudioProtectionTime may be needed on Android
// as well.
inline constexpr base::TimeDelta kTabAudioProtectionTime = base::TimeDelta();
#else
// Time during which a tab cannot be discarded after having played audio.
inline constexpr base::TimeDelta kTabAudioProtectionTime = base::Minutes(1);
#endif

// LINT.IfChange(CanDiscardResult)
// Whether a page can be discarded. These values are persisted to logs. Entries
// should not be renumbered and numeric values should never be reused.
enum class CanDiscardResult {
  // The page can be discarded. The user should experience minimal disruption
  // from discarding.
  kEligible = 0,
  // The page can be discarded. The user will likely find discarding disruptive.
  kProtected = 1,
  // The page cannot be discarded.
  kDisallowed = 2,

  kMaxValue = kDisallowed,
};
// LINT.ThenChange(//tools/metrics/histograms/enums.xml:CanDiscardResult)

// Caches page node properties to facilitate sorting.
class PageNodeSortProxy {
 public:
  PageNodeSortProxy(base::WeakPtr<const PageNode> page_node,
                    CanDiscardResult can_discard_result,
                    bool is_visible,
                    bool is_focused,
                    base::TimeTicks last_visibility_change_time);
  PageNodeSortProxy(PageNodeSortProxy&&);
  PageNodeSortProxy& operator=(PageNodeSortProxy&&);
  ~PageNodeSortProxy();

  base::WeakPtr<const PageNode> page_node() const { return page_node_; }
  bool is_disallowed() const {
    return can_discard_result_ == CanDiscardResult::kDisallowed;
  }
  bool is_protected() const {
    return can_discard_result_ == CanDiscardResult::kProtected;
  }
  bool is_visible() const { return is_visible_; }
  bool is_focused() const { return is_focused_; }
  base::TimeTicks last_visibility_change_time() const {
    return last_visibility_change_time_;
  }

  // Returns true if the rhs is more important.
  bool operator<(const PageNodeSortProxy& rhs) const {
    if (is_disallowed() != rhs.is_disallowed()) {
      return rhs.is_disallowed();
    }
    if (is_focused_ != rhs.is_focused_) {
      return rhs.is_focused_;
    }
    if (is_visible_ != rhs.is_visible_) {
      return rhs.is_visible_;
    }
    if (is_protected() != rhs.is_protected()) {
      return rhs.is_protected();
    }
    return last_visibility_change_time_ < rhs.last_visibility_change_time_;
  }

 private:
  base::WeakPtr<const PageNode> page_node_;
  CanDiscardResult can_discard_result_;
  bool is_visible_;
  bool is_focused_;
  base::TimeTicks last_visibility_change_time_;
};

// DiscardEligibilityPolicy decides which PageNode is eligigle for tab
// discarding.
class DiscardEligibilityPolicy
    : public GraphOwnedAndRegistered<DiscardEligibilityPolicy>,
      public NodeDataDescriberDefaultImpl,
      public PageNodeObserver {
 public:
  // Export discard reason in the public interface.
  using DiscardReason = ::mojom::LifecycleUnitDiscardReason;

  DiscardEligibilityPolicy();
  ~DiscardEligibilityPolicy() override;
  DiscardEligibilityPolicy(const DiscardEligibilityPolicy& other) = delete;
  DiscardEligibilityPolicy& operator=(const DiscardEligibilityPolicy&) = delete;

  // PageNodeObserver:
  void OnMainFrameDocumentChanged(const PageNode* page_node) override;

  base::WeakPtr<DiscardEligibilityPolicy> GetWeakPtr() {
    return weak_factory_.GetWeakPtr();
  }

  void SetNoDiscardPatternsForProfile(
      const base::UnguessableToken& browser_context_id,
      const std::vector<std::string>& patterns);
  void ClearNoDiscardPatternsForProfile(
      const base::UnguessableToken& browser_context_id);

  // Indicates if the page will be immediately perceptible to the users after
  // discard.
  //
  // This is a subset of CanDiscard() and is used by
  // DiscardPageWithCrashedSubframePolicy.
  bool WillDiscardBePerceptible(const PageNode* page_node) const;

  // Indicates if `page_node` can be discarded. This mostly focuses on whether
  // the page is in the discard blocklist.
  bool IsDiscardAllowed(const PageNode* page_node) const;

  // Indicates if `page_node` can be urgently discarded, using a list of
  // criteria depending on `discard_reason`. Uses the default background
  // protection window unless `ignore_recent_visibility` is true (which
  // uses a zero duration window).
  CanDiscardResult CanDiscard(
      const PageNode* page_node,
      DiscardReason discard_reason,
      bool ignore_recent_visibility = false,
      std::vector<CannotDiscardReason>* cannot_discard_reasons = nullptr) const;

  // Similar to `CanDiscard`, but uses a custom background protection window
  // specified by `minimum_time_in_background`.
  CanDiscardResult CanDiscardWithCustomRecentVisibilityWindow(
      const PageNode* page_node,
      DiscardReason discard_reason,
      base::TimeDelta minimum_time_in_background,
      std::vector<CannotDiscardReason>* cannot_discard_reasons = nullptr) const;

  // This must be called from PageDiscardingHelper or from test only.
  static void AddDiscardAttemptMarker(PageNode* page_node);
  static void RemovesDiscardAttemptMarkerForTesting(PageNode* page_node);

  // Sets an additional callback that should be invoked whenever the
  // SetNoDiscardPatternsForProfile() or ClearNoDiscardPatternsForProfile()
  // methosd is called, with the method's `browser_context_id` argument.
  void SetOptOutPolicyChangedCallback(
      base::RepeatingCallback<void(const base::UnguessableToken&)> callback);

  bool IsPageOptedOutOfDiscarding(
      const base::UnguessableToken& browser_context_id,
      const GURL& url) const;

  void set_always_discard_for_testing(bool always_discard) {
    always_discard_for_testing_ = always_discard;
  }

 private:
  void OnPassedToGraph(Graph* graph) override;
  void OnTakenFromGraph(Graph* graph) override;

  // Records UMA metrics about the discard decision and the state of the
  // page node at the time of the decision.
  void RecordDiscardDecisionMetrics(
      const PageNode* page_node,
      DiscardReason discard_reason,
      CanDiscardResult result,
      base::span<const CannotDiscardReason> protection_reasons) const;

  // NodeDataDescriber implementation:
  base::DictValue DescribePageNodeData(const PageNode* node) const override;

  std::map<base::UnguessableToken, std::unique_ptr<url_matcher::URLMatcher>>
      profiles_no_discard_patterns_ GUARDED_BY_CONTEXT(sequence_checker_);

  base::RepeatingCallback<void(const base::UnguessableToken&)>
      opt_out_policy_changed_callback_ GUARDED_BY_CONTEXT(sequence_checker_);

  bool always_discard_for_testing_ = false;

  SEQUENCE_CHECKER(sequence_checker_);

  base::WeakPtrFactory<DiscardEligibilityPolicy> weak_factory_{this};
};

}  // namespace performance_manager::policies

#endif  // CHROME_BROWSER_PERFORMANCE_MANAGER_POLICIES_DISCARD_ELIGIBILITY_POLICY_H_
