// 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.

#ifndef COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_FEATURE_PROMO_PRECONDITION_H_
#define COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_FEATURE_PROMO_PRECONDITION_H_

#include <memory>
#include <string>
#include <vector>

#include "base/functional/callback.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "components/user_education/common/feature_promo/feature_promo_result.h"
#include "components/user_education/common/feature_promo/impl/typed_data.h"
#include "components/user_education/common/feature_promo/impl/typed_data_collection.h"
#include "ui/base/identifier/typed_identifier.h"
#include "ui/base/identifier/unique_identifier.h"

namespace user_education {

namespace internal {
class FeaturePromoQueueCachedDataTest;
class FeaturePromoQueueSetCachedDataTest;
}  // namespace internal

// Represents a single precondition for promos. Derive specific preconditions
// from this class.
class FeaturePromoPrecondition {
 public:
  DECLARE_UNIQUE_IDENTIFIER_TYPE(PreconditionIdentifier);
  using CachedDataIdentifier = UnownedTypedDataCollection::UntypedIdentifier;

  // Boilerplate; this class is not copyable.
  FeaturePromoPrecondition(const FeaturePromoPrecondition&) = delete;
  void operator=(const FeaturePromoPrecondition&) = delete;
  virtual ~FeaturePromoPrecondition() = default;

  // Returns a unique identifier for different types of preconditions.
  virtual PreconditionIdentifier GetIdentifier() const = 0;

  // Gets a description of the precondition.
  virtual const std::string& GetDescription() const = 0;

  // Gets whether the precondition is met and promos are allowed. If not,
  // returns the relevant failure.
  //
  // When preconditions are being computed together, `data` contains all
  // information computed by previous preconditions. This is necessarily order-
  // dependent; a precondition cannot retrieve data computed by another
  // precondition that is evaluated after it.
  virtual FeaturePromoResult CheckPrecondition(
      UnownedTypedDataCollection& data) const = 0;

  // Extracts any cached data from this precondition and adds it to `to_add_to`;
  // future calls to this object may fail. Cached data likely reflects the most
  // recent time `IsAllowed()` was called, and therefore that method should
  // always be called first.
  virtual void ExtractCachedData(OwnedTypedDataCollection& to_add_to) {}

 protected:
  FeaturePromoPrecondition() = default;
};

// Same as `FeaturePromoPrecondition`, but stores values for identifier
// and description, along with optional cached data.
class FeaturePromoPreconditionBase : public FeaturePromoPrecondition {
 public:
  // Boilerplate; this class is not copyable.
  ~FeaturePromoPreconditionBase() override;

  // FeaturePromoPrecondition:
  PreconditionIdentifier GetIdentifier() const override;
  const std::string& GetDescription() const override;
  void ExtractCachedData(OwnedTypedDataCollection& to_add_to) override;

 protected:
  FeaturePromoPreconditionBase(PreconditionIdentifier identifier,
                               std::string description);

  // Use this method to initialize the various types of data the precondition
  // will support by passing in appropriate typed identifiers.
  //
  // Can be called any number of times with unique typed identifiers, or all
  // at once.
  template <typename... Args>
  void InitCache(ui::TypedIdentifier<CachedDataIdentifier, Args>... args) {
    (data_.Insert(std::make_unique<TypedData<Args>>(args)), ...);
  }

  // Use this method to initialize a single piece of cached data the
  // precondition will support with an initial value constructed from `args`.
  template <typename T, typename... Args>
  void InitCachedData(ui::TypedIdentifier<CachedDataIdentifier, T> id,
                      Args&&... args) {
    data_.Emplace(id, std::forward<Args>(args)...);
  }

  // Retrieve a reference to cached data held by the precondition, which can be
  // used to get or set the value. InitCache() must have been called with
  // the same `id`, and `ExtractData()` must not have been called.
  //
  // The data returned is mutable even thought the method is const, because it
  // is expected to be used to cache data.
  //
  // A readonly reference to the cached data is also stored in `data` to ensure
  // that it is available to later preconditions in the list.
  template <typename T>
  T& GetCachedDataForComputation(
      UnownedTypedDataCollection& data,
      ui::TypedIdentifier<CachedDataIdentifier, T> id) const {
    data.AddFrom(id.identifier(), data_);
    return data[id];
  }

 private:
  FRIEND_TEST_ALL_PREFIXES(FeaturePromoPreconditionTest, SetAndGetCachedData);
  FRIEND_TEST_ALL_PREFIXES(FeaturePromoPreconditionTest,
                           SetAndGetCachedDataDifferentPreconditions);
  FRIEND_TEST_ALL_PREFIXES(FeaturePromoPreconditionTest,
                           GetCachedDataCrashesIfDataNotPresent);
  FRIEND_TEST_ALL_PREFIXES(FeaturePromoPreconditionTest,
                           GetCachedDataCrashesIfCacheCollision);
  FRIEND_TEST_ALL_PREFIXES(FeaturePromoPreconditionTest, ExtractCachedData);
  FRIEND_TEST_ALL_PREFIXES(FeaturePromoPreconditionTest,
                           GetAfterExtractCachedDataFails);
  FRIEND_TEST_ALL_PREFIXES(FeaturePromoPreconditionTest,
                           FeaturePromoPreconditionList_ExtractCachedData);
  friend internal::FeaturePromoQueueCachedDataTest;
  friend internal::FeaturePromoQueueSetCachedDataTest;

  const PreconditionIdentifier identifier_;
  const std::string description_;

  // Mutable so that data can be cached during retrieval.
  mutable OwnedTypedDataCollection data_;
};

// Represents a precondition that returns a cached value that is updated as it
// changes in realtime.
class CachingFeaturePromoPrecondition : public FeaturePromoPreconditionBase {
 public:
  CachingFeaturePromoPrecondition(PreconditionIdentifier identifier,
                                  std::string description,
                                  FeaturePromoResult initial_state);
  ~CachingFeaturePromoPrecondition() override;

  // FeaturePromoPrecondition:
  FeaturePromoResult CheckPrecondition(
      UnownedTypedDataCollection&) const override;

  // See `set_is_allowed`.
  void set_check_result_for_testing(FeaturePromoResult check_result) {
    set_check_result(check_result);
  }

 protected:
  // Called by implementing classes to update the allowed state.
  void set_check_result(FeaturePromoResult check_result) {
    check_result_ = check_result;
  }

 private:
  FeaturePromoResult check_result_;
};

// Represents a precondition that forwards its allowed state from some other
// source of truth via a callback.
class CallbackFeaturePromoPrecondition : public FeaturePromoPreconditionBase {
 public:
  using SimpleCallback = base::RepeatingCallback<FeaturePromoResult()>;
  using CallbackWithData = base::RepeatingCallback<FeaturePromoResult(
      UnownedTypedDataCollection& data)>;
  CallbackFeaturePromoPrecondition(PreconditionIdentifier identifier,
                                   std::string description,
                                   SimpleCallback check_result_callback);
  CallbackFeaturePromoPrecondition(PreconditionIdentifier identifier,
                                   std::string description,
                                   CallbackWithData check_result_callback);
  ~CallbackFeaturePromoPrecondition() override;

  // FeaturePromoPrecondition:
  FeaturePromoResult CheckPrecondition(
      UnownedTypedDataCollection& data) const override;

 private:
  const CallbackWithData check_result_callback_;
};

// Represents a precondition that forwards all of its information from another
// (longer-lived) source precondition.
class ForwardingFeaturePromoPrecondition : public FeaturePromoPrecondition {
 public:
  explicit ForwardingFeaturePromoPrecondition(
      const FeaturePromoPrecondition& source);
  ~ForwardingFeaturePromoPrecondition() override;

  // FeaturePromoPrecondition:
  PreconditionIdentifier GetIdentifier() const override;
  const std::string& GetDescription() const override;
  FeaturePromoResult CheckPrecondition(
      UnownedTypedDataCollection& data) const override;

 protected:
  // Can be called by derived classes to clear out references and avoid UAF.
  // After calling this method, `CheckPrecondition()` will return an error.
  void Invalidate();

 private:
  // The source precondition. If it becomes invalid, this is reset to null, and
  // future calls to `CheckPrecondition()` will return an error.
  raw_ptr<const FeaturePromoPrecondition> source_ = nullptr;

  // Preserves the identifier even after a call to `Invalidate()`.
  const PreconditionIdentifier cached_identifier_;

  // Preserves the description even after a call to `Invalidate()`.
  const std::string cached_description_;
};

// Represents an ordered list of preconditions which will be checked (see
// `CheckPreconditions()`). Owns the precondition objects it contains.
//
// Preconditions are created per-list; if state needs to be maintained between
// creation of lists, a forwarding- or callback-based implementation can be
// used.
class FeaturePromoPreconditionList {
 public:
  using ListType = std::vector<std::unique_ptr<FeaturePromoPrecondition>>;

  // Represents the result of checking the precondition list.
  class CheckResult {
   public:
    CheckResult() = default;
    CheckResult(
        FeaturePromoResult result,
        FeaturePromoPrecondition::PreconditionIdentifier failed_precondition)
        : result_(result), failed_precondition_(failed_precondition) {}
    CheckResult(const CheckResult&) = default;
    CheckResult& operator=(const CheckResult&) = default;
    ~CheckResult() = default;

    FeaturePromoResult result() const { return result_; }
    std::optional<FeaturePromoResult::Failure> failure() const {
      return result_.failure();
    }
    FeaturePromoPrecondition::PreconditionIdentifier failed_precondition()
        const {
      return failed_precondition_;
    }
    explicit operator bool() const { return result_; }
    bool operator!() const { return !result_; }
    bool operator==(const CheckResult&) const = default;

   private:
    // The result of checking the list; success if no preconditions failed.
    FeaturePromoResult result_;
    // The identifier of the precondition that failed, or a null if none did.
    FeaturePromoPrecondition::PreconditionIdentifier failed_precondition_;
  };

  template <typename... Args>
  explicit FeaturePromoPreconditionList(Args... preconditions) {
    (AddPrecondition(std::move(preconditions)), ...);
  }

  FeaturePromoPreconditionList(FeaturePromoPreconditionList&&) noexcept;
  FeaturePromoPreconditionList& operator=(
      FeaturePromoPreconditionList&&) noexcept;
  ~FeaturePromoPreconditionList();

  // Adds `precondition` to this list.
  void AddPrecondition(std::unique_ptr<FeaturePromoPrecondition> precondition);

  // Appends all of the preconditions from `other` to this list.
  void AppendAll(FeaturePromoPreconditionList other);

  // Checks that all preconditions in the list are met, in order, and returns
  // either the `failure()` and `identifier()` of the first that does not pass,
  // or `FeaturePromoResult::Success()` if all preconditions pass.
  //
  // Computed values will be read from/stored in `computed_data`.
  CheckResult CheckPreconditions(
      UnownedTypedDataCollection& computed_data) const;

  // Extracts cached data from all preconditions into `to_add_to`.
  void ExtractCachedData(OwnedTypedDataCollection& to_add_to);

 private:
  ListType preconditions_;
};

}  // namespace user_education

// These macros are used to declare `FeaturePromoPrecondition::Identifier`s.
// Use these instead of the element identifier ones in case the implementation
// of the precondition IDs changes.
#define DECLARE_FEATURE_PROMO_PRECONDITION_IDENTIFIER_VALUE(IdentifierName) \
  DECLARE_UNIQUE_IDENTIFIER_VALUE(                                          \
      ::user_education::FeaturePromoPrecondition::PreconditionIdentifier,   \
      IdentifierName)
#define DEFINE_FEATURE_PROMO_PRECONDITION_IDENTIFIER_VALUE(IdentifierName) \
  DEFINE_ELEMENT_IDENTIFIER_VALUE(IdentifierName)
#define DEFINE_LOCAL_FEATURE_PROMO_PRECONDITION_IDENTIFIER_VALUE(         \
    IdentifierName)                                                       \
  DEFINE_MACRO_LOCAL_UNIQUE_IDENTIFIER_VALUE(                             \
      __FILE__, __LINE__,                                                 \
      ::user_education::FeaturePromoPrecondition::PreconditionIdentifier, \
      IdentifierName)

#define DECLARE_CLASS_PROMO_PRECONDITION_CACHED_DATA(Type, Name)              \
  DECLARE_CLASS_TYPED_IDENTIFIER_VALUE(                                       \
      ::user_education::FeaturePromoPrecondition::CachedDataIdentifier, Type, \
      Name)
#define DEFINE_CLASS_PROMO_PRECONDITION_CACHED_DATA(Class, Type, Name)         \
  DEFINE_CLASS_TYPED_IDENTIFIER_VALUE(                                         \
      Class, ::user_education::FeaturePromoPrecondition::CachedDataIdentifier, \
      Type, Name)

#endif  // COMPONENTS_USER_EDUCATION_COMMON_FEATURE_PROMO_FEATURE_PROMO_PRECONDITION_H_
