// Copyright 2018 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_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_REGISTRY_CACHE_H_
#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_REGISTRY_CACHE_H_

#include <functional>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/compiler_specific.h"
#include "base/component_export.h"
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "base/observer_list_types.h"
#include "base/sequence_checker.h"
#include "components/account_id/account_id.h"
#include "components/services/app_service/public/cpp/app.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/app_update.h"

namespace apps {

// An in-memory cache of all the metadata about installed apps known to App
// Service. Can be queried synchronously for information about the current
// state, and can be observed to receive updates about changes to that app
// state.
//
// AppServiceProxy sees a stream of `apps::AppPtr` "deltas", or changes in app
// state, received from publishers. This cache stores the "sum" of those
// previous deltas. When a new delta is received, observers are presented with
// an `apps:::AppUpdate` containing information about what has changed, and
// then the new delta is "added" to the stored state.
//
// This class is not thread-safe.
//
// See components/services/app_service/README.md for more details.
class COMPONENT_EXPORT(APP_UPDATE) AppRegistryCache {
 public:
  // Observer for changes to app state in the AppRegistryCache.
  class COMPONENT_EXPORT(APP_UPDATE) Observer : public base::CheckedObserver {
   public:
    Observer(const Observer&) = delete;
    Observer& operator=(const Observer&) = delete;

    // Called whenever AppRegistryCache receives an update for any app. `update`
    // exposes the latest field values and whether they have changed in this
    // update (as per the docs on `apps::AppUpdate`). The `update` argument
    // shouldn't be accessed after OnAppUpdate returns.
    virtual void OnAppUpdate(const AppUpdate& update) {}

    // Called when the AppRegistryCache first receives a set of apps for
    // `app_type`. This is called after reading from the AppStorage file.
    // Note that this will not be called for app types initialized prior to this
    // observer being registered. Observers should call
    // AppRegistryCache::InitializedAppTypes() at the time of starting
    // observation to get a set of the app types which have been initialized.
    virtual void OnAppTypeInitialized(apps::AppType app_type) {}

    // Called whenever AppRegistryCache.OnApps is called with
    // `should_notify_initialized` is true, when the publisher publishes apps
    // for the first time after the system startup. AppRegistryCache's internal
    // variables haven't been updated, so `states_` and `deltas_in_progress_`
    // are having the old app info, not include any new app info in `delta`.
    //
    // Please use OnAppTypeInitialized if possible.
    virtual void OnAppTypePublishing(const std::vector<AppPtr>& deltas,
                                     apps::AppType app_type) {}

    // Called when the AppRegistryCache object (the thing that this observer
    // observes) will be destroyed. In response, the observer, `this`, should
    // call "cache->RemoveObserver(this)", whether directly or indirectly (e.g.
    // via base::ScopedObservation::Reset).
    virtual void OnAppRegistryCacheWillBeDestroyed(AppRegistryCache* cache) = 0;

   protected:
    Observer();

    ~Observer() override;
  };

  AppRegistryCache();

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

  ~AppRegistryCache();

  // Prefer using a base::ScopedObservation for idiomatic observer behavior.
  void AddObserver(Observer* observer);
  void RemoveObserver(Observer* observer);

  AppType GetAppType(std::string_view app_id);

  std::vector<AppPtr> GetAllApps();

  void SetAccountId(const AccountId& account_id);

  // Calls f, a void-returning function whose arguments are (const
  // apps::AppUpdate&), on each app in the cache.
  //
  // f's argument is an apps::AppUpdate instead of an apps::AppPtr so
  // that callers can more easily share code with Observer::OnAppUpdate (which
  // also takes an apps::AppUpdate), and an apps::AppUpdate also has a
  // StateIsNull method.
  //
  // The apps::AppUpdate argument to f shouldn't be accessed after f returns.
  //
  // f must be synchronous, and if it asynchronously calls ForEachApp again,
  // it's not guaranteed to see a consistent state.
  template <typename FunctionType>
  void ForEachApp(FunctionType f) const {
    DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);

    for (const auto& s_iter : states_) {
      const App* state = s_iter.second.get();

      auto d_iter = deltas_in_progress_.find(s_iter.first);
      const App* delta =
          (d_iter != deltas_in_progress_.end()) ? d_iter->second : nullptr;

      f(AppUpdate(state, delta, account_id_));
    }

    for (const auto& d_iter : deltas_in_progress_) {
      const App* delta = d_iter.second;

      if (states_.contains(d_iter.first)) {
        continue;
      }

      f(AppUpdate(nullptr, delta, account_id_));
    }
  }

  // Returns an `apps::AppUpdate` corresponding to the app in the cache with the
  // given `app_id`, or `nullopt` if there is not such an app.
  //
  // The `apps::AppUpdate` view may dangle when the state of the cache changes,
  // and should not be accessed after this happens.
  std::optional<AppUpdate> GetAppUpdate(std::string_view app_id) const;

  // Calls f, a void-returning function whose arguments are (const
  // apps::AppUpdate&), on the app in the cache with the given app_id. It will
  // return true (and call f) if there is such an app, otherwise it will return
  // false (and not call f). The AppUpdate argument to f has the same semantics
  // as for ForEachApp, above.
  //
  // f must be synchronous, and if it asynchronously calls ForOneApp again,
  // it's not guaranteed to see a consistent state.
  template <typename FunctionType>
  bool ForOneApp(std::string_view app_id, FunctionType f) const {
    std::optional<AppUpdate> app_update = GetAppUpdate(app_id);
    if (app_update.has_value()) {
      f(*app_update);
      return true;
    }
    return false;
  }

  // Returns the set of app types that have so far been initialized.
  const std::set<AppType>& InitializedAppTypes() const;

  // Returns true after the initialization of `app_type` is done and the apps of
  // `app_type` is ready in AppRegistryCache.
  //
  // Note: The app publisher and app platform might be not ready yet when
  // IsAppTypeInitialized returns true.
  //
  // The initialization is considered done when the apps for `app_type` have
  // been added to AppRegistryCache, when `InitApps` is called by AppStorage or
  // when `should_notify_initialized` is set as true by the publisher of
  // `app_type`.
  bool IsAppTypeInitialized(AppType app_type) const;

  // Returns true after both the publisher and the app platform of `app_type`
  // are ready.
  //
  // Note: apps of `app_type` might haven't been added to AppRegistryCache when
  // IsAppTypePublished returns true.
  //
  // The app type published is considered true when the publisher of `app_type`
  // publishes apps when `should_notify_initialized` is set as true.
  //
  // When apps of `app_type` are initialized and added to AppRegistryCache, the
  // publisher may be not ready yet, so IsAppTypeInitialized could return true,
  // but IsAppTypePublished could be false. Only when the publisher publishes
  // apps with `should_notify_initialized` as true, IsAppTypePublished returns
  // true.
  //
  // Please use IsAppTypeInitialized if possible. This interface can be used to
  // check whether the publisher and the app platform for `app_type` are ready
  // for `app_type`.
  bool IsAppTypePublished(AppType app_type) const;

  // Returns true if the cache contains an app with id `app_id` whose
  // `Readiness()` corresponds to an installed state.
  bool IsAppInstalled(std::string_view app_id) const;

  // Clears all apps from the cache.
  void ReinitializeForTesting();

  // Please use AppServiceProxy::OnApps if possible. This method is used to
  // tests without Profile, e.g. unittests.
  void OnAppsForTesting(std::vector<AppPtr> deltas,
                        apps::AppType app_type,
                        bool should_notify_initialized);

 private:
  friend class AppRegistryCacheTest;
  friend class AppRegistryCacheWrapperTest;
  friend class PublisherTest;
  friend class AppServiceProxyAsh;
  friend class AppServiceProxyBase;

  // Called by AppServiceProxy::OnApps when publishers publish changes on apps,
  // to notifies all observers of state-and-delta AppUpdate's and then merges
  // the cached states with the deltas. If `should_notify_initialized` is true,
  // notify observers `app_type` has been initialized by calling
  // `OnAppTypeInitialized`.
  //
  // Please use AppServiceProxy::OnApps if possible. For tests without Profile,
  // e.g. unittests, please use OnAppsForTesting.
  void OnApps(std::vector<AppPtr> deltas,
              apps::AppType app_type,
              bool should_notify_initialized);

  // Notifies all observers of state-and-delta AppUpdate's (the state comes
  // from the internal cache, the delta comes from the argument) and then
  // merges the cached states with the deltas. This interface can be used to
  // update apps for multiple app types, and it won't notify observers the
  // initialized status.
  //
  // Notification and merging might be delayed until after OnApps returns. For
  // example, suppose that the initial set of states is (a0, b0, c0) for three
  // app_id's ("a", "b", "c"). Now suppose OnApps is called with two updates
  // (b1, c1), and when notified of b1, an observer calls OnApps again with
  // (c2, d2). The c1 delta should be processed before the c2 delta, as it was
  // sent first: c2 should be merged (with "newest wins" semantics) onto c1 and
  // not vice versa. This means that processing c2 (scheduled by the second
  // OnApps call) should wait until the first OnApps call has finished
  // processing b1 (and then c1), which means that processing c2 is delayed
  // until after the second OnApps call returns.
  //
  // The callee will consume the deltas. An apps::AppPtr has the ownership
  // semantics of a unique_ptr, and will be deleted when out of scope. The
  // caller presumably calls OnApps(std::move(deltas)).
  void OnApps(std::vector<AppPtr> deltas);

  void DoOnApps(std::vector<AppPtr> deltas);

  // Notifies all observers that apps of `app_type` have been initialized.
  void InitApps(apps::AppType app_type);

  void OnAppTypeInitialized();

  // TODO(crbug.com/484371187): Investigate if reentrancy can be removed.
  base::ObserverList<
      Observer,
      /*check_empty=*/false,
      base::ObserverListReentrancyPolicy::kAllowReentrancyUntriaged>
      observers_;

  // Maps from app_id to the latest state: the "sum" of all previous deltas.
  std::map<std::string, AppPtr, std::less<>> states_;

  // Track the deltas being processed or are about to be processed by OnApps.
  // They are separate to manage the "notification and merging might be delayed
  // until after OnApps returns" concern described above.
  //
  // OnApps calls DoOnApps zero or more times. If we're nested, so that there's
  // multiple OnApps call to this AppRegistryCache in the call stack, the
  // deeper OnApps call simply adds work to deltas_pending_ and returns
  // without calling DoOnApps. If we're not nested, OnApps calls DoOnApps one or
  // more times; "more times" happens if DoOnApps notifying observers leads to
  // more OnApps calls that enqueue deltas_pending_ work. The
  // deltas_in_progress_ map (keyed by app_id) contains those deltas being
  // considered by DoOnApps.
  //
  // Nested OnApps calls are expected to be rare (but still dealt with
  // sensibly). In the typical case, OnApps should call DoOnApps exactly once,
  // and deltas_pending_ will stay empty.
  std::map<std::string, raw_ptr<App, CtnExperimental>, std::less<>>
      deltas_in_progress_;
  std::vector<AppPtr> deltas_pending_;

  // Saves app types which will finish initialization, and OnAppTypeInitialized
  // will be called to notify observers.
  std::set<AppType> in_progress_initialized_app_types_;

  // Saves app types which have finished initialization, and
  // OnAppTypeInitialized has been called to notify observers.
  std::set<AppType> initialized_app_types_;

  // Saves app types which have been published by the publisher, and
  // OnAppTypePublished has been called to notify observers.
  std::set<AppType> published_app_types_;

  AccountId account_id_;

  SEQUENCE_CHECKER(my_sequence_checker_);
};

}  // namespace apps

#endif  // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_REGISTRY_CACHE_H_
