// Copyright 2021 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_SHARE_SHARE_RANKING_H_
#define CHROME_BROWSER_SHARE_SHARE_RANKING_H_

#include <optional>

#include "base/callback_list.h"
#include "base/containers/flat_map.h"
#include "base/memory/weak_ptr.h"
#include "base/supports_user_data.h"
#include "build/build_config.h"
#include "chrome/browser/share/proto/share_ranking_message.pb.h"
#include "chrome/browser/share/share_history.h"
#include "components/leveldb_proto/public/proto_database.h"

class Profile;

namespace sharing {

class ShareHistory;

class ShareRanking : public base::SupportsUserData::Data {
 public:
  using Ranking = std::vector<std::string>;

  using BackingDb = leveldb_proto::ProtoDatabase<proto::ShareRanking>;
  using GetRankingCallback =
      base::OnceCallback<void(std::optional<Ranking> result)>;

  static ShareRanking* Get(Profile* profile);

  explicit ShareRanking(Profile* profile,
                        std::unique_ptr<BackingDb> backing_db = nullptr);
  ~ShareRanking() override;

  void UpdateRanking(const std::string& type, Ranking ranking);
  void GetRanking(const std::string& type, GetRankingCallback callback);

  // This method:
  // 1. Fetches the existing rankings and histories for |type| from the provided
  //    databases
  // 2. Runs the ranking algorithm (see below).
  // 3. If |persist_update|, writes the new ranking back to |ranking|
  // 4. Returns the display ranking to use for this specific share
  void Rank(ShareHistory* history,
            const std::string& type,
            const std::vector<std::string>& available_on_system,
            size_t fold,
            size_t length,
            bool persist_update,
            GetRankingCallback callback);

  void Clear(const base::Time& start = base::Time(),
             const base::Time& end = base::Time());

  // The core of the ranking algorithm, exposed as a pure function for ease of
  // testing and reasoning about the code. This function takes the existing
  // share history and ranking for this type, a set of all targets available on
  // the current system, and a length, and computes the new display ranking and
  // the new persistent ranking.
  //
  // TODO(ellyjones): Document (publicly) how this works and why.
  static void ComputeRanking(
      const std::map<std::string, int>& all_share_history,
      const std::map<std::string, int>& recent_share_history,
      const Ranking& old_ranking,
      const std::vector<std::string>& available_on_system,
      size_t fold,
      size_t length,
      Ranking* display_ranking,
      Ranking* persisted_ranking);

  static const char* const kMoreTarget;

  void set_initial_ranking_for_test(Ranking ranking) {
    initial_ranking_for_test_ = ranking;
  }

 private:
  // A PendingRankCall represents a call to Rank() that is in progress; an
  // object of this type is threaded through the async calls used by Rank() to
  // pass state from one step to the next.
  struct PendingRankCall {
    PendingRankCall();
    ~PendingRankCall();

    // These members mirror the parameters passed in to Rank().
    std::string type;
    std::vector<std::string> available_on_system;
    size_t fold;
    size_t length;
    bool persist_update;
    GetRankingCallback callback;

    // This is the ShareHistory passed into Rank(), held as a WeakPtr in case
    // the Rank() call is pending across profile teardown.
    base::WeakPtr<ShareHistory> history_db;

    // These are intermediate results generated by async steps of Rank(),
    // accumulated into the PendingRankCall for later use.
    std::vector<ShareHistory::Target> all_history;
    std::vector<ShareHistory::Target> recent_history;
  };

  void Init();
  void OnInitDone(leveldb_proto::Enums::InitStatus status);
  void OnBackingGetDone(std::string key,
                        GetRankingCallback callback,
                        bool ok,
                        std::unique_ptr<proto::ShareRanking> ranking);

  void FlushToBackingDb(const std::string& key);

  // Steps of the state machine for Rank() - in order, that method:
  // 1. Fetches all history for the given type
  // 2. Fetches recent history for the given type
  // 3. Fetches the existing ranking for the given type
  // Each of these steps is asynchronous and the On*Done methods are used for
  // their completion callbacks.
  void OnRankGetAllDone(std::unique_ptr<PendingRankCall> pending,
                        std::vector<ShareHistory::Target> history);
  void OnRankGetRecentDone(std::unique_ptr<PendingRankCall> pending,
                           std::vector<ShareHistory::Target> history);
  void OnRankGetOldRankingDone(std::unique_ptr<PendingRankCall> pending,
                               std::optional<Ranking> ranking);

  // Return the default initial ranking, which depends on the current locale.
  Ranking GetDefaultInitialRankingForType(const std::string& type);

  bool init_finished_ = false;
  leveldb_proto::Enums::InitStatus db_init_status_;

  base::OnceClosureList post_init_callbacks_;

  std::unique_ptr<BackingDb> db_;

  // Cached ranking values, which are used when populated; otherwise they are
  // fetched from the backing database. Updates to this are written back to the
  // backing database by FlushToBackingDb().
  base::flat_map<std::string, Ranking> ranking_;

  std::optional<Ranking> initial_ranking_for_test_;

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

}  // namespace sharing

#endif  // CHROME_BROWSER_SHARE_SHARE_RANKING_H_
