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

#ifndef COMPONENTS_NTP_SNIPPETS_CONTENT_SUGGESTIONS_SERVICE_H_
#define COMPONENTS_NTP_SNIPPETS_CONTENT_SUGGESTIONS_SERVICE_H_

#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>

#include "base/callback_forward.h"
#include "base/observer_list.h"
#include "base/optional.h"
#include "base/scoped_observer.h"
#include "base/task/cancelable_task_tracker.h"
#include "base/time/time.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/history_service_observer.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/ntp_snippets/breaking_news/breaking_news_gcm_app_handler.h"
#include "components/ntp_snippets/callbacks.h"
#include "components/ntp_snippets/category.h"
#include "components/ntp_snippets/category_rankers/category_ranker.h"
#include "components/ntp_snippets/category_status.h"
#include "components/ntp_snippets/content_suggestions_provider.h"
#include "components/ntp_snippets/logger.h"
#include "components/ntp_snippets/remote/remote_suggestions_scheduler.h"
#include "components/ntp_snippets/user_classifier.h"
#include "services/identity/public/cpp/identity_manager.h"

class PrefService;
class PrefRegistrySimple;

namespace favicon {
class LargeIconService;
}  // namespace favicon

namespace favicon_base {
struct LargeIconImageResult;
}  // namespace favicon_base

namespace ntp_snippets {

class RemoteSuggestionsProvider;

// Retrieves suggestions from a number of ContentSuggestionsProviders and serves
// them grouped into categories. There can be at most one provider per category.
class ContentSuggestionsService : public KeyedService,
                                  public ContentSuggestionsProvider::Observer,
                                  public identity::IdentityManager::Observer,
                                  public history::HistoryServiceObserver {
 public:
  class Observer {
   public:
    // Fired every time the service receives a new set of data for the given
    // |category|, replacing any previously available data (though in most cases
    // there will be an overlap and only a few changes within the data). The new
    // data is then available through |GetSuggestionsForCategory(category)|.
    virtual void OnNewSuggestions(Category category) = 0;

    // Fired when the status of a suggestions category changed. Note that for
    // some status changes, the UI must update immediately (e.g. to remove
    // invalidated suggestions). See comments on the individual CategoryStatus
    // values for details.
    virtual void OnCategoryStatusChanged(Category category,
                                         CategoryStatus new_status) = 0;

    // Fired when a suggestion has been invalidated. The UI must immediately
    // clear the suggestion even from open NTPs. Invalidation happens, for
    // example, when the content that the suggestion refers to is gone.
    // Note that this event may be fired even if the corresponding category is
    // not currently AVAILABLE, because open UIs may still be showing the
    // suggestion that is to be removed. This event may also be fired for
    // |suggestion_id|s that never existed and should be ignored in that case.
    virtual void OnSuggestionInvalidated(
        const ContentSuggestion::ID& suggestion_id) = 0;

    // Fired when the previously sent data is not valid anymore and a refresh
    // of all the suggestions is required. Called for example when the sign in
    // state changes and personalised suggestions have to be shown or discarded.
    virtual void OnFullRefreshRequired() = 0;

    // Sent when the service is shutting down. After the service has shut down,
    // it will not provide any data anymore, though calling the getters is still
    // safe.
    virtual void ContentSuggestionsServiceShutdown() = 0;

   protected:
    virtual ~Observer() = default;
  };

  enum class State {
    ENABLED,
    DISABLED,
  };

  ContentSuggestionsService(
      State state,
      identity::IdentityManager*
          identity_manager,                      // Can be nullptr in unittests.
      history::HistoryService* history_service,  // Can be nullptr in unittests.
      // Can be nullptr in unittests.
      favicon::LargeIconService* large_icon_service,
      PrefService* pref_service,
      std::unique_ptr<CategoryRanker> category_ranker,
      std::unique_ptr<UserClassifier> user_classifier,
      std::unique_ptr<RemoteSuggestionsScheduler>
          remote_suggestions_scheduler,  // Can be nullptr in unittests.
      std::unique_ptr<Logger> debug_logger);
  ~ContentSuggestionsService() override;

  // Inherited from KeyedService.
  void Shutdown() override;

  static void RegisterProfilePrefs(PrefRegistrySimple* registry);

  State state() { return state_; }

  // Gets all categories for which a provider is registered. The categories may
  // or may not be available, see |GetCategoryStatus()|. The order in which the
  // categories are returned is the order in which they should be displayed.
  std::vector<Category> GetCategories() const;

  // Gets the status of a category.
  CategoryStatus GetCategoryStatus(Category category) const;

  // Gets the meta information of a category.
  base::Optional<CategoryInfo> GetCategoryInfo(Category category) const;

  // Gets the available suggestions for a category. The result is empty if the
  // category is available and empty, but also if the category is unavailable
  // for any reason, see |GetCategoryStatus()|.
  const std::vector<ContentSuggestion>& GetSuggestionsForCategory(
      Category category) const;

  // Fetches the image for the suggestion with the given |suggestion_id| and
  // runs the |callback|. If that suggestion doesn't exist or the fetch fails,
  // the callback gets an empty image. The callback will not be called
  // synchronously.
  void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id,
                            ImageFetchedCallback callback);

  // Fetches the image data for the suggestion with the given |suggestion_id|
  // and runs the |callback|. If that suggestion doesn't exist or the fetch
  // fails, the callback gets empty data. The callback will not be called
  // synchronously.
  void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id,
                                ImageDataFetchedCallback callback);

  // Fetches the favicon from local cache (if larger than or equal to
  // |minimum_size_in_pixel|) or from Google server (if there is no icon in the
  // cache) and returns the results in the callback. If that suggestion doesn't
  // exist or the fetch fails, the callback gets an empty image. The callback
  // will not be called synchronously.
  void FetchSuggestionFavicon(const ContentSuggestion::ID& suggestion_id,
                              int minimum_size_in_pixel,
                              int desired_size_in_pixel,
                              ImageFetchedCallback callback);

  // Dismisses the suggestion with the given |suggestion_id|, if it exists.
  // This will not trigger an update through the observers (i.e. providers must
  // not call |Observer::OnNewSuggestions|).
  void DismissSuggestion(const ContentSuggestion::ID& suggestion_id);

  // Dismisses the given |category|, if it exists.
  // This will not trigger an update through the observers.
  void DismissCategory(Category category);

  // Restores all dismissed categories.
  // This will not trigger an update through the observers.
  void RestoreDismissedCategories();

  // Returns whether |category| is dismissed.
  bool IsCategoryDismissed(Category category) const;

  // Fetches additional contents for the given |category|. If the fetch was
  // completed, the given |callback| is called with the updated content.
  // This includes new and old data.
  // TODO(jkrcal): Consider either renaming this to FetchMore or unify the ways
  // to get suggestions to just this async Fetch() API.
  void Fetch(const Category& category,
             const std::set<std::string>& known_suggestion_ids,
             FetchDoneCallback callback);

  // Reloads suggestions from all categories, from all providers. If a provider
  // naturally has some ability to generate fresh suggestions, it may provide a
  // completely new set of suggestions. If the provider has no ability to
  // generate fresh suggestions on demand, it may only fill in any vacant space
  // by suggestions that were previously not included due to space limits (there
  // may be vacant space because of the user dismissing suggestions in the
  // meantime).
  void ReloadSuggestions();

  // Observer accessors.
  void AddObserver(Observer* observer);
  void RemoveObserver(Observer* observer);

  // Registers a new ContentSuggestionsProvider. It must be ensured that at most
  // one provider is registered for every category and that this method is
  // called only once per provider.
  void RegisterProvider(std::unique_ptr<ContentSuggestionsProvider> provider);

  // Removes history from the specified time range where the URL matches the
  // |filter| from all providers. The data removed depends on the provider. Note
  // that the data outside the time range may be deleted, for example
  // suggestions, which are based on history from that time range. Providers
  // should immediately clear any data related to history from the specified
  // time range where the URL matches the |filter|.
  void ClearHistory(base::Time begin,
                    base::Time end,
                    const base::Callback<bool(const GURL& url)>& filter);

  // Removes all suggestions from all caches or internal stores in all
  // providers. It does, however, not remove any suggestions from the provider's
  // sources, so if its configuration hasn't changed, it might return the same
  // results when it fetches the next time. In particular, calling this method
  // will not mark any suggestions as dismissed.
  void ClearAllCachedSuggestions();

  // Only for debugging use through the internals page.
  // Retrieves suggestions of the given |category| that have previously been
  // dismissed and are still stored in the respective provider. If the
  // provider doesn't store dismissed suggestions, the callback receives an
  // empty vector. The callback may be called synchronously.
  void GetDismissedSuggestionsForDebugging(
      Category category,
      DismissedSuggestionsCallback callback);

  // Only for debugging use through the internals page. Some providers
  // internally store a list of dismissed suggestions to prevent them from
  // reappearing. This function clears all suggestions of the given |category|
  // from such lists, making dismissed suggestions reappear (if the provider
  // supports it).
  void ClearDismissedSuggestionsForDebugging(Category category);

  std::string GetDebugLog() const {
    return debug_logger_->GetHumanReadableLog();
  }

  // Returns true if the remote suggestions provider is enabled.
  bool AreRemoteSuggestionsEnabled() const;

  // The reference to the RemoteSuggestionsProvider provider should
  // only be set by the factory and only used for debugging.
  // TODO(jkrcal) The way we deal with the circular dependency feels wrong.
  // Consider swapping the dependencies: first constructing all providers, then
  // constructing the service (passing the remote provider as arg), finally
  // registering the service as an observer of all providers?
  // TODO(jkrcal) Move the getter into the scheduler interface (the setter is
  // then not needed any more). crbug.com/695447
  void set_remote_suggestions_provider(
      RemoteSuggestionsProvider* remote_suggestions_provider) {
    remote_suggestions_provider_ = remote_suggestions_provider;
  }
  RemoteSuggestionsProvider* remote_suggestions_provider_for_debugging() {
    return remote_suggestions_provider_;
  }

  // The interface is suited for informing about external events that have
  // influence on scheduling remote fetches. Can be nullptr in tests.
  RemoteSuggestionsScheduler* remote_suggestions_scheduler() {
    return remote_suggestions_scheduler_.get();
  }

  // Can be nullptr in tests.
  // TODO(jkrcal): The getter is only used from the bridge and from
  // snippets-internals. Can we get rid of it with the metrics refactoring?
  UserClassifier* user_classifier() { return user_classifier_.get(); }

  CategoryRanker* category_ranker() { return category_ranker_.get(); }

  Logger* debug_logger() { return debug_logger_.get(); }

 private:
  friend class ContentSuggestionsServiceTest;

  // Implementation of ContentSuggestionsProvider::Observer.
  void OnNewSuggestions(ContentSuggestionsProvider* provider,
                        Category category,
                        std::vector<ContentSuggestion> suggestions) override;
  void OnCategoryStatusChanged(ContentSuggestionsProvider* provider,
                               Category category,
                               CategoryStatus new_status) override;
  void OnSuggestionInvalidated(
      ContentSuggestionsProvider* provider,
      const ContentSuggestion::ID& suggestion_id) override;

  // identity::IdentityManager::Observer implementation.
  void OnPrimaryAccountSet(const AccountInfo& account_info) override;
  void OnPrimaryAccountCleared(const AccountInfo& account_info) override;

  // history::HistoryServiceObserver implementation.
  void OnURLsDeleted(history::HistoryService* history_service,
                     const history::DeletionInfo& deletion_info) override;
  void HistoryServiceBeingDeleted(
      history::HistoryService* history_service) override;

  // Registers the given |provider| for the given |category|, unless it is
  // already registered. Returns true if the category was newly registered or
  // false if it is dismissed or was present before.
  bool TryRegisterProviderForCategory(ContentSuggestionsProvider* provider,
                                      Category category);
  void RegisterCategory(Category category,
                        ContentSuggestionsProvider* provider);
  void UnregisterCategory(Category category,
                          ContentSuggestionsProvider* provider);

  // Removes a suggestion from the local store |suggestions_by_category_|, if it
  // exists. Returns true if a suggestion was removed.
  bool RemoveSuggestionByID(const ContentSuggestion::ID& suggestion_id);

  // Fires the OnCategoryStatusChanged event for the given |category|.
  void NotifyCategoryStatusChanged(Category category);

  void OnSignInStateChanged(bool has_signed_in);

  // Re-enables a dismissed category, making querying its provider possible.
  void RestoreDismissedCategory(Category category);

  void RestoreDismissedCategoriesFromPrefs();
  void StoreDismissedCategoriesToPrefs();

  // Not implemented for articles. For all other categories, destroys its
  // provider, deletes all mentions (except from dismissed list) and notifies
  // observers that the category is disabled.
  void DestroyCategoryAndItsProvider(Category category);

  // Get the domain of the suggestion suitable for fetching the favicon.
  GURL GetFaviconDomain(const ContentSuggestion::ID& suggestion_id);

  // Initiate the fetch of a favicon from the local cache.
  void GetFaviconFromCache(const GURL& publisher_url,
                           int minimum_size_in_pixel,
                           int desired_size_in_pixel,
                           ImageFetchedCallback callback,
                           bool continue_to_google_server);

  // Callbacks for fetching favicons.
  void OnGetFaviconFromCacheFinished(
      const GURL& publisher_url,
      int minimum_size_in_pixel,
      int desired_size_in_pixel,
      ImageFetchedCallback callback,
      bool continue_to_google_server,
      const favicon_base::LargeIconImageResult& result);
  void OnGetFaviconFromGoogleServerFinished(
      const GURL& publisher_url,
      int minimum_size_in_pixel,
      int desired_size_in_pixel,
      ImageFetchedCallback callback,
      favicon_base::GoogleFaviconServerRequestStatus status);

  // Whether the content suggestions feature is enabled.
  State state_;

  // All registered providers, owned by the service.
  std::vector<std::unique_ptr<ContentSuggestionsProvider>> providers_;

  // All registered categories and their providers. A provider may be contained
  // multiple times, if it provides multiple categories. The keys of this map
  // are exactly the entries of |categories_| and the values are a subset of
  // |providers_|.
  std::map<Category, ContentSuggestionsProvider*, Category::CompareByID>
      providers_by_category_;

  // All dismissed categories and their providers. These may be restored by
  // RestoreDismissedCategories(). The provider can be null if the dismissed
  // category has received no updates since initialisation.
  // (see RestoreDismissedCategoriesFromPrefs())
  std::map<Category, ContentSuggestionsProvider*, Category::CompareByID>
      dismissed_providers_by_category_;

  // All current suggestion categories in arbitrary order. This vector contains
  // exactly the same categories as |providers_by_category_|.
  std::vector<Category> categories_;

  // All current suggestions grouped by category. This contains an entry for
  // every category in |categories_| whose status is an available status. It may
  // contain an empty vector if the category is available but empty (or still
  // loading).
  std::map<Category, std::vector<ContentSuggestion>, Category::CompareByID>
      suggestions_by_category_;

  // Observer for the IdentityManager. All observers are notified when the
  // signin state changes so that they can refresh their list of suggestions.
  ScopedObserver<identity::IdentityManager, identity::IdentityManager::Observer>
      identity_manager_observer_;

  // Observer for the HistoryService. All providers are notified when history is
  // deleted.
  ScopedObserver<history::HistoryService, history::HistoryServiceObserver>
      history_service_observer_;

  base::ObserverList<Observer> observers_;

  const std::vector<ContentSuggestion> no_suggestions_;

  base::CancelableTaskTracker favicons_task_tracker_;

  // Keep a direct reference to this special provider to redirect debugging
  // calls to it. If the RemoteSuggestionsProvider is loaded, it is also present
  // in |providers_|, otherwise this is a nullptr.
  RemoteSuggestionsProvider* remote_suggestions_provider_;

  favicon::LargeIconService* large_icon_service_;

  PrefService* pref_service_;

  // Interface for informing about external events that have influence on
  // scheduling remote fetches.
  std::unique_ptr<RemoteSuggestionsScheduler> remote_suggestions_scheduler_;

  // Classifies the user on the basis of long-term user interactions.
  std::unique_ptr<UserClassifier> user_classifier_;

  // Provides order for categories.
  std::unique_ptr<CategoryRanker> category_ranker_;

  std::unique_ptr<Logger> debug_logger_;

  DISALLOW_COPY_AND_ASSIGN(ContentSuggestionsService);
};

}  // namespace ntp_snippets

#endif  // COMPONENTS_NTP_SNIPPETS_CONTENT_SUGGESTIONS_SERVICE_H_
