// Copyright 2022 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_SAVED_TAB_GROUPS_INTERNAL_SAVED_TAB_GROUP_MODEL_H_
#define COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_SAVED_TAB_GROUP_MODEL_H_

#include <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "base/uuid.h"
#include "components/saved_tab_groups/public/saved_tab_group.h"
#include "components/saved_tab_groups/public/saved_tab_group_tab.h"
#include "components/saved_tab_groups/public/types.h"
#include "components/sync/base/collaboration_id.h"
#include "components/sync/protocol/saved_tab_group_specifics.pb.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "google_apis/gaia/gaia_id.h"

namespace tab_groups {

class SavedTabGroupModelObserver;
class SavedTabGroup;

// Serves to maintain the current state of all saved tab groups in the current
// session.
class SavedTabGroupModel {
 public:
  SavedTabGroupModel();
  SavedTabGroupModel(const SavedTabGroupModel&) = delete;
  SavedTabGroupModel& operator=(const SavedTabGroupModel& other) = delete;
  ~SavedTabGroupModel();

  // Accessor for the underlying storage vector. Prefer the methods below to
  // distinguish between the saved and shared tab groups.
  const std::vector<SavedTabGroup>& saved_tab_groups() const {
    return saved_tab_groups_;
  }

  // Returns saved tab groups (which are not shared). The returned pointers can
  // be invalidated on any model's mutation.
  std::vector<const SavedTabGroup*> GetSavedTabGroupsOnly() const;

  // Returns shared tab groups. The returned pointers can be invalidated on any
  // model's mutation.
  std::vector<const SavedTabGroup*> GetSharedTabGroupsOnly() const;

  bool is_loaded() { return is_loaded_; }

  // Returns the index of the SavedTabGroup if it exists in the vector. Else
  // std::nullopt.
  std::optional<int> GetIndexOf(const LocalTabGroupID local_group_id) const;
  std::optional<int> GetIndexOf(const base::Uuid& id) const;

  // Return weather the group is pinned if it exists in the vector. Else
  // std::nullopt.
  std::optional<bool> IsGroupPinned(const base::Uuid& id) const;

  // Get a pointer to the SavedTabGroup from an ID. Returns nullptr if not in
  // vector.
  const SavedTabGroup* Get(const LocalTabGroupID local_group_id) const;
  const SavedTabGroup* Get(const base::Uuid& id) const;

  // Methods for checking if a group is in the SavedTabGroupModel.
  bool Contains(const LocalTabGroupID& local_group_id) const {
    return GetIndexOf(local_group_id).has_value();
  }
  bool Contains(const base::Uuid& id) const {
    return GetIndexOf(id).has_value();
  }

  // Helper for getting number of SavedTabGroups in the vector.
  int Count() const { return saved_tab_groups_.size(); }

  // Helper for getting empty state of the SavedTabGroup vector.
  bool IsEmpty() const { return Count() <= 0; }

  // Add / Remove / Update a single tab group from the model. For local changes
  // that should be synced.
  void AddedLocally(SavedTabGroup saved_group);
  void RemovedLocally(const LocalTabGroupID local_group_id);
  void RemovedLocally(const base::Uuid& id);
  void UpdateVisualDataLocally(
      const LocalTabGroupID local_group_id,
      const tab_groups::TabGroupVisualData* visual_data);

  // Does not notify observers or create a deep copy. Rather this directly sets
  // the collaboration_id on the group. It is up to callers to ensure the
  // updated group is retrieved from the service before use.
  void MakeTabGroupSharedForTesting(const LocalTabGroupID& local_group_id,
                                    syncer::CollaborationId collaboration_id);
  void MakeTabGroupUnsharedForTesting(const LocalTabGroupID& local_group_id);

  // Mark whether the tab group identified by `local_group_id` is transitioning
  // to a saved group.
  void SetIsTransitioningToSaved(const LocalTabGroupID& local_group_id,
                                 bool is_transitioning_to_saved);

  // Pin SavedTabGroup if it's unpinned. Unpin SavedTabGroup if it's pinned.
  void TogglePinState(base::Uuid id);

  // Similar to the Add/Remove/Update but originate from sync. As such, these
  // function do not notify sync observers of these changes to avoid looping
  // calls.
  void AddedFromSync(SavedTabGroup saved_group);
  void RemovedFromSync(const LocalTabGroupID local_group_id);
  void RemovedFromSync(const base::Uuid& id);
  void UpdatedVisualDataFromSync(
      const LocalTabGroupID local_group_id,
      const tab_groups::TabGroupVisualData* visual_data);
  void UpdatedVisualDataFromSync(
      const base::Uuid& id,
      const tab_groups::TabGroupVisualData* visual_data);

  const SavedTabGroup* GetGroupContainingTab(
      const base::Uuid& saved_tab_guid) const;
  const SavedTabGroup* GetGroupContainingTab(
      const LocalTabID& local_tab_id) const;

  // Adds a saved tab to `index` in the specified group denoted by `group_id` if
  // it exists. Notify local observers if the tab was added locally, and sync
  // observers if it was added from sync.
  void AddTabToGroupLocally(const base::Uuid& group_id, SavedTabGroupTab tab);
  void AddTabToGroupFromSync(const base::Uuid& group_id, SavedTabGroupTab tab);

  // Calls the UpdateTab method on a group found by group id in the model.
  // Calls the observer function SavedTabGroupUpdatedLocally, if
  // `notify_observers` is true.
  void UpdateTabInGroup(const base::Uuid& group_id,
                        SavedTabGroupTab tab,
                        bool notify_observers);

  // Updates `tab` with a new `local_id`. Unlike `UpdateTabInGroup`, this method
  // does not notify observers, as this is not a change we want to sync.
  void UpdateLocalTabId(const base::Uuid& group_id,
                        SavedTabGroupTab tab,
                        std::optional<LocalTabID> local_id);

  // Removes saved tab `tab_id` in the specified group denoted by `group_id` if
  // it exists. The group is deleted if the last tab is removed from it.
  // Notifies observers if the tab was removed locally.
  void RemoveTabFromGroupLocally(
      const base::Uuid& group_id,
      const base::Uuid& tab_id,
      std::optional<GaiaId> local_gaia_id = std::nullopt);

  // Similar to above but the group with `group_id` must exist. Notifies
  // observers that the tab was removed from sync. `removed_by` is the user who
  // removed the tab group (may be empty, e.g. if unknown), populated for shared
  // tab groups only. If `prevent_group_destruction_for_testing` is set to true,
  // then the group will not be removed as a result of calling this method on
  // the last tab in the group. This should only be used for testing, since
  // there are no cases where the group should live after the tab is deleted,
  // except during a race condition in sync.
  void RemoveTabFromGroupFromSync(
      const base::Uuid& group_id,
      const base::Uuid& tab_id,
      GaiaId removed_by = GaiaId(),
      bool prevent_group_destruction_for_testing = false);

  // Moves a saved tab from its current position to `index` in the specified
  // group denoted by `group_id` if it exists.
  void MoveTabInGroupTo(const base::Uuid& group_id,
                        const base::Uuid& tab_id,
                        int index);

  // Attempts to merge the remote group metadata or tab with the local object
  // that holds the same `guid`.
  const SavedTabGroup* MergeRemoteGroupMetadata(
      const base::Uuid& guid,
      const std::u16string& title,
      TabGroupColorId color,
      std::optional<size_t> position,
      std::optional<std::string> creator_cache_guid,
      std::optional<std::string> last_updater_cache_guid,
      base::Time update_time,
      const GaiaId& updated_by);
  const SavedTabGroupTab* MergeRemoteTab(const SavedTabGroupTab& remote_tab);

  // Changes the index of a given tab group by id. The new index provided is the
  // expected index after the group is removed. Notify local observers if the
  // group was reordered locally, and sync observers if the group was reordered
  // from sync.
  void ReorderGroupLocally(const base::Uuid& id, int new_index);
  void ReorderGroupFromSync(const base::Uuid& id, int new_index);

  // Reorders the group with `id` to be before or after the group with
  // `next_id` or `prev_id`.
  void ReorderGroupBefore(const base::Uuid& id, const base::Uuid& next_id);
  void ReorderGroupAfter(const base::Uuid& id, const base::Uuid& prev_id);

  // Update the creator cache guid for all saved groups that have
  // `old_cache_guid`, to `new_cache_guid`.
  std::pair<std::set<base::Uuid>, std::set<base::Uuid>> UpdateLocalCacheGuid(
      std::optional<std::string> old_cache_guid,
      std::optional<std::string> new_cache_guid);

  // Update the last interaction time with the group.
  void UpdateLastUserInteractionTimeLocally(
      const LocalTabGroupID& local_group_id);

  // Update the last seen time for a tab.
  void UpdateTabLastSeenTimeFromLocal(const base::Uuid& group_id,
                                      const base::Uuid& tab_id);
  void UpdateTabLastSeenTimeFromSync(const base::Uuid& group_id,
                                     const base::Uuid& tab_id,
                                     base::Time time);

  // Update the position for a share group from sync. If the position is
  // nullopt, the group will be moved to the end of the list.
  void UpdatePositionForSharedGroupFromSync(const base::Uuid& group_id,
                                            std::optional<size_t> position);

  // Update the last updater cache guid for a give group and optionally a tab.
  void UpdateLastUpdaterCacheGuidForGroup(
      const std::optional<std::string>& cache_guid,
      const LocalTabGroupID& group_id,
      const std::optional<LocalTabID>& tab_id);

  // Updates the shared attribution for a given group. This method does not
  // notify observers as this method should be called together with other
  // changes which would notify observers anyway.
  void UpdateSharedAttribution(const LocalTabGroupID& group_id,
                               const std::optional<LocalTabID>& tab_id,
                               GaiaId updated_by);

  // Loads the model from the storage. `tabs` must have a corresponding group in
  // `groups`.
  void LoadStoredEntries(std::vector<SavedTabGroup> groups,
                         std::vector<SavedTabGroupTab> tabs);

  // Functions that should be called when a SavedTabGroup's corresponding
  // TabGroup is closed or opened.
  void OnGroupOpenedInTabStrip(const base::Uuid& id,
                               const LocalTabGroupID& local_group_id);
  void OnGroupClosedInTabStrip(const LocalTabGroupID& local_group_id);

  // Add/Remove observers for this model.
  void AddObserver(SavedTabGroupModelObserver* observer);
  void RemoveObserver(SavedTabGroupModelObserver* observer);

  // Start transitioning a shared tab group to a saved group. `shared_group_id`
  // is the ID of the shared group.
  // TODO(crbug.com/396143520): Rename this method to
  // StartTransitioningToShared().
  void MarkTransitionedToShared(const base::Uuid& shared_group_id);

  // Marks that a tab group is hidden and should not be shown to users.
  void SetGroupHidden(const base::Uuid& group_id);

  // Restores the hidden state of a tab group.
  void RestoreHiddenGroupFromSync(const base::Uuid& group_id);

  // Called to notify of the sync bridge state changes, e.g. whether initial
  // merge or disable sync are in progress. Invoked only for shared tab group
  // bridge.
  void OnSyncBridgeUpdateTypeChanged(
      SyncBridgeUpdateType sync_bridge_update_type);

  // Update the archival status and archival timestamp of the local tab group.
  void UpdateArchivalStatus(const base::Uuid& id, bool archivalStatus);

  // Update bookmark node id of the local tab group.
  void UpdateBookmarkNodeId(const base::Uuid& id,
                            const std::optional<base::Uuid>& bookmark_node_id);

 private:
  // Returns mutable group containing tab with ID `saved_tab_guid`, otherwise
  // returns null.
  SavedTabGroup* MutableGroupContainingTab(const base::Uuid& saved_tab_guid);
  SavedTabGroup* GetMutableGroup(const LocalTabGroupID& local_group_id);
  SavedTabGroup* GetMutableGroup(const base::Uuid& id);

  // Moves the group denoted by `id` to the position `new_index`.
  void ReorderGroupImpl(const base::Uuid& id, int new_index);

  // Updates all group positions to match the index they are currently stored
  // at.
  void UpdateGroupPositionsImpl();

  // Insert `group` into sorted order based on its position compared to already
  // stored groups in `saved_tab_groups_`. It should be noted that
  // `saved_tab_groups` must already be in sorted order for this function to
  // work as intended. To do this, UpdatePositionsImpl() can be called.
  void InsertGroupImpl(SavedTabGroup group);

  // Implementations of CRUD operations.
  SavedTabGroup RemoveImpl(size_t index);
  void UpdateVisualDataImpl(int index,
                            const tab_groups::TabGroupVisualData* visual_data);

  // Pending NTP related operations. Pending NTP is a placeholder NTP
  // automatically created when a group from sync reaches zero-tabs state to
  // make it easier for UI to handle since UI today doesn't support zero-tab tab
  // groups in any platform. Zero-tab state is a valid transient state since
  // concurrent tab additions and removals are common in shared tab groups. A
  // pending NTP exists locally in the model and the UI, but not synced. Any
  // incoming / outgoing navigations or tab additions will commit this tab to
  // sync. There can only be one maximum pending NTP in a group and it will be
  // the only tab in the group.
  void CreatePendingNtp(SavedTabGroup& group);
  void StartSyncingPendingNtpIfAny(SavedTabGroup& group);
  void MergePendingNtpWithIncomingTabIfAny(SavedTabGroup& group,
                                           const base::Uuid& tab_id);
  SavedTabGroupTab* FindPendingNtpInGroup(SavedTabGroup& group);

  void HandleTabGroupRemovedFromSync(int index);

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

  // True when SavedTabGroupModel::LoadStoredEntries has finished, false
  // otherwise.
  bool is_loaded_ = false;

  // Storage of all saved tab groups in the order they are displayed. The
  // position of the groups must maintain sorted order as sync may not propagate
  // an entire update completely leaving us with missing groups / gaps between
  // the positions.
  std::vector<SavedTabGroup> saved_tab_groups_;
};

}  // namespace tab_groups

#endif  // COMPONENTS_SAVED_TAB_GROUPS_INTERNAL_SAVED_TAB_GROUP_MODEL_H_
