chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/grid/base_grid_mediator.mm

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

#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/base_grid_mediator.h"

#import <UIKit/UIKit.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

#import <memory>

#import "base/debug/dump_without_crashing.h"
#import "base/functional/bind.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/scoped_multi_source_observation.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "components/bookmarks/common/bookmark_pref_names.h"
#import "components/prefs/pref_service.h"
#import "components/saved_tab_groups/tab_group_sync_service.h"
#import "components/tab_groups/tab_group_visual_data.h"
#import "ios/chrome/browser/commerce/model/shopping_persisted_data_tab_helper.h"
#import "ios/chrome/browser/default_browser/model/utils.h"
#import "ios/chrome/browser/drag_and_drop/model/drag_item_util.h"
#import "ios/chrome/browser/iph_for_new_chrome_user/model/tab_based_iph_browser_agent.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/reading_list/model/reading_list_browser_agent.h"
#import "ios/chrome/browser/saved_tab_groups/model/ios_tab_group_sync_util.h"
#import "ios/chrome/browser/saved_tab_groups/model/tab_group_sync_service_factory.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/url/url_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/browser_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group_utils.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/bookmarks_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/reading_list_add_command.h"
#import "ios/chrome/browser/shared/public/commands/tab_grid_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_grid_toolbar_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_groups_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/shared/ui/util/url_with_title.h"
#import "ios/chrome/browser/snapshots/model/model_swift.h"
#import "ios/chrome/browser/snapshots/model/snapshot_browser_agent.h"
#import "ios/chrome/browser/snapshots/model/snapshot_id_wrapper.h"
#import "ios/chrome/browser/snapshots/model/snapshot_storage_wrapper.h"
#import "ios/chrome/browser/snapshots/model/snapshot_tab_helper.h"
#import "ios/chrome/browser/tabs/model/inactive_tabs/features.h"
#import "ios/chrome/browser/tabs_search/model/tabs_search_service.h"
#import "ios/chrome/browser/tabs_search/model/tabs_search_service_factory.h"
#import "ios/chrome/browser/ui/menu/action_factory.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_drag_drop_metrics.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_item_identifier.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_mediator_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_toolbars_mutator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_utils.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/selected_grid_items.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_context_menu/tab_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_idle_status_handler.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_metrics.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_mode_holder.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_mode_observing.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_configuration.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_group_action_type.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_group_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_utils.h"
#import "ios/chrome/browser/ui/tab_switcher/web_state_tab_switcher_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "net/base/apple/url_conversions.h"
#import "ui/gfx/image/image.h"

using PinnedState = WebStateSearchCriteria::PinnedState;

namespace {

void LogPriceDropMetrics(web::WebState* web_state) {
  ShoppingPersistedDataTabHelper* shopping_helper =
      ShoppingPersistedDataTabHelper::FromWebState(web_state);
  if (!shopping_helper) {
    return;
  }
  const ShoppingPersistedDataTabHelper::PriceDrop* price_drop =
      shopping_helper->GetPriceDrop();
  BOOL has_price_drop =
      price_drop && price_drop->current_price && price_drop->previous_price;
  base::RecordAction(base::UserMetricsAction(
      base::StringPrintf("Commerce.TabGridSwitched.%s",
                         has_price_drop ? "HasPriceDrop" : "NoPriceDrop")
          .c_str()));
}

// Returns the Browser with `identifier` in its WebStateList. Returns `nullptr`
// if not found.
Browser* GetBrowserForNonPinnedTabWithId(BrowserList* browser_list,
                                         web::WebStateID identifier,
                                         bool is_otr_tab) {
  const BrowserList::BrowserType browser_types =
      is_otr_tab ? BrowserList::BrowserType::kIncognito
                 : BrowserList::BrowserType::kRegularAndInactive;
  std::set<Browser*> browsers = browser_list->BrowsersOfType(browser_types);
  for (Browser* browser : browsers) {
    WebStateList* web_state_list = browser->GetWebStateList();
    int index = GetWebStateIndex(web_state_list,
                                 WebStateSearchCriteria{
                                     .identifier = identifier,
                                     .pinned_state = PinnedState::kNonPinned,
                                 });
    if (index != WebStateList::kInvalidIndex) {
      return browser;
    }
  }
  return nullptr;
}

}  // namespace

@interface BaseGridMediator () <CRWWebStateObserver,
                                SnapshotStorageObserver,
                                TabGridModeObserving>
// The browser state from the browser.
@property(nonatomic, readonly) ChromeBrowserState* browserState;

@end

@implementation BaseGridMediator {
  // Observers for WebStateList.
  std::unique_ptr<WebStateListObserverBridge> _webStateListObserverBridge;
  std::unique_ptr<
      base::ScopedMultiSourceObservation<WebStateList, WebStateListObserver>>
      _scopedWebStateListObservation;
  // Observer for WebStates.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
  std::unique_ptr<
      base::ScopedMultiSourceObservation<web::WebState, web::WebStateObserver>>
      _scopedWebStateObservation;

  // The current Browser.
  base::WeakPtr<Browser> _browser;

  // Items selected for editing.
  SelectedGridItems* _selectedEditingItems;

  // Holder for the current mode of the Tab Grid.
  TabGridModeHolder* _modeHolder;
}

- (instancetype)initWithModeHolder:(TabGridModeHolder*)modeHolder {
  if ((self = [super init])) {
    CHECK(modeHolder);
    _modeHolder = modeHolder;
    [modeHolder addObserver:self];
    _webStateListObserverBridge =
        std::make_unique<WebStateListObserverBridge>(self);
    _scopedWebStateListObservation = std::make_unique<
        base::ScopedMultiSourceObservation<WebStateList, WebStateListObserver>>(
        _webStateListObserverBridge.get());
    _webStateObserverBridge =
        std::make_unique<web::WebStateObserverBridge>(self);
    _scopedWebStateObservation =
        std::make_unique<base::ScopedMultiSourceObservation<
            web::WebState, web::WebStateObserver>>(
            _webStateObserverBridge.get());
  }
  return self;
}

#pragma mark - Public properties

- (Browser*)browser {
  return _browser.get();
}

- (void)setBrowser:(Browser*)browser {
  [self.snapshotStorage removeObserver:self];
  _scopedWebStateListObservation->RemoveAllObservations();
  _scopedWebStateObservation->RemoveAllObservations();

  _browser.reset();
  if (browser) {
    _browser = browser->AsWeakPtr();
  }

  _webStateList = browser ? browser->GetWebStateList() : nullptr;
  _browserState = browser ? browser->GetBrowserState() : nullptr;

  [self.snapshotStorage addObserver:self];

  if (_webStateList) {
    _scopedWebStateListObservation->AddObservation(_webStateList);
    [self addWebStateObservations];
    _selectedEditingItems =
        [[SelectedGridItems alloc] initWithWebStateList:_webStateList];

    if (self.webStateList->count() > 0) {
      [self populateConsumerItems];
    }
  }
}

- (void)setConsumer:(id<TabCollectionConsumer>)consumer {
  _consumer = consumer;
  [self resetToAllItems];
  [consumer setTabGridMode:_modeHolder.mode];
}

#pragma mark - Subclassing

- (TabGridModeHolder*)modeHolder {
  return _modeHolder;
}

- (void)disconnect {
  _browser.reset();
  _browserState = nil;
  _consumer = nil;
  _delegate = nil;
  _toolbarsMutator = nil;
  _containedGridToolbarsProvider = nil;
  _tabGridHandler = nil;
  _gridConsumer = nil;
  _tabPresentationDelegate = nil;

  _scopedWebStateListObservation->RemoveAllObservations();
  _scopedWebStateObservation->RemoveAllObservations();
  _scopedWebStateObservation.reset();
  _scopedWebStateListObservation.reset();

  _webStateObserverBridge.reset();
  _webStateList->RemoveObserver(_webStateListObserverBridge.get());
  _webStateListObserverBridge.reset();
  _webStateList = nil;

  [_modeHolder removeObserver:self];
  _modeHolder = nil;
}

- (void)configureToolbarsButtons {
  NOTREACHED() << "Should be implemented in a subclass.";
}

- (void)configureButtonsInSelectionMode:
    (TabGridToolbarsConfiguration*)configuration {
  NSUInteger selectedItemsCount = _selectedEditingItems.tabsCount;
  NSUInteger selectedShareableItemsCount =
      _selectedEditingItems.sharableTabsCount;

  BOOL allItemsSelected =
      static_cast<int>(selectedItemsCount) ==
      (self.webStateList->count() - self.webStateList->pinned_tabs_count());

  configuration.selectAllButton = !allItemsSelected;
  configuration.deselectAllButton = allItemsSelected;
  configuration.doneButton = YES;
  configuration.closeSelectedTabsButton = selectedItemsCount > 0;
  configuration.shareButton = selectedShareableItemsCount > 0;
  if (IsTabGroupInGridEnabled()) {
    configuration.addToButton = selectedItemsCount > 0;
  } else {
    configuration.addToButton = selectedShareableItemsCount > 0;
  }
  configuration.selectedItemsCount = selectedItemsCount;

  configuration.addToButtonMenu =
      [UIMenu menuWithChildren:[self addToButtonMenuElements]];
}

- (void)displayActiveTab {
  NOTREACHED() << "Should be implemented in a subclass.";
}

- (void)populateConsumerItems {
  if (!self.webStateList) {
    return;
  }
  [self.consumer populateItems:CreateItems(self.webStateList)
        selectedItemIdentifier:[self activeIdentifier]];
}

- (GridItemIdentifier*)activeIdentifier {
  WebStateList* webStateList = self.webStateList;
  if (!webStateList) {
    return nil;
  }

  int webStateIndex = webStateList->active_index();
  if (webStateIndex == WebStateList::kInvalidIndex) {
    return nil;
  }

  if (IsTabGroupInGridEnabled()) {
    const TabGroup* group = webStateList->GetGroupOfWebStateAt(webStateIndex);
    if (group) {
      return [GridItemIdentifier groupIdentifier:group
                                withWebStateList:webStateList];
    }
  }

  return [GridItemIdentifier
      tabIdentifier:webStateList->GetWebStateAt(webStateIndex)];
}

- (void)updateForTabInserted {
  // Default implementation is a no-op.
}

- (void)addWebStateObservations {
  int firstIndex =
      IsPinnedTabsEnabled() ? self.webStateList->pinned_tabs_count() : 0;
  for (int i = firstIndex; i < self.webStateList->count(); i++) {
    web::WebState* webState = self.webStateList->GetWebStateAt(i);
    [self addObservationForWebState:webState];
  }
}

- (void)addObservationForWebState:(web::WebState*)webState {
  _scopedWebStateObservation->AddObservation(webState);
}

- (void)removeObservationForWebState:(web::WebState*)webState {
  _scopedWebStateObservation->RemoveObservation(webState);
}

- (void)insertNewWebStateAtGridIndex:(int)index withURL:(const GURL&)newTabURL {
  // The incognito mediator's Browser is briefly set to nil after the last
  // incognito tab is closed.  This occurs because the incognito BrowserState
  // needs to be destroyed to correctly clear incognito browsing data.  Don't
  // attempt to create a new WebState with a nil BrowserState.
  if (!self.browser) {
    return;
  }

  // There are some circumstances where a new tab insertion can be erroniously
  // triggered while another web state list mutation is happening. To ensure
  // those bugs don't become crashes, check that the web state list is OK to
  // mutate.
  if (self.webStateList->IsMutating()) {
    // Shouldn't have happened!
    DCHECK(false) << "Reentrant web state insertion!";
    return;
  }

  DCHECK(self.browserState);
  web::WebState::CreateParams params(self.browserState);
  std::unique_ptr<web::WebState> webState = web::WebState::Create(params);

  int webStateListIndex =
      WebStateIndexFromGridDropItemIndex(self.webStateList, index);
  webStateListIndex =
      std::clamp(webStateListIndex, 0, self.webStateList->count());

  web::NavigationManager::WebLoadParams loadParams(newTabURL);
  loadParams.transition_type = ui::PAGE_TRANSITION_TYPED;
  webState->GetNavigationManager()->LoadURLWithParams(loadParams);

  self.webStateList->InsertWebState(
      std::move(webState),
      WebStateList::InsertionParams::AtIndex(webStateListIndex).Activate());
}

- (void)insertItem:(GridItemIdentifier*)item
    beforeWebStateIndex:(int)nextWebStateIndex {
  WebStateList* webStateList = self.webStateList;
  GridItemIdentifier* nextItemIdentifier;
  if (webStateList->ContainsIndex(nextWebStateIndex)) {
    const TabGroup* group =
        webStateList->GetGroupOfWebStateAt(nextWebStateIndex);
    if (group) {
      nextItemIdentifier = [GridItemIdentifier groupIdentifier:group
                                              withWebStateList:webStateList];
    } else {
      nextItemIdentifier = [GridItemIdentifier
          tabIdentifier:self.webStateList->GetWebStateAt(nextWebStateIndex)];
    }
  }
  [self.consumer insertItem:item
                beforeItemID:nextItemIdentifier
      selectedItemIdentifier:[self activeIdentifier]];
}

- (void)moveItem:(GridItemIdentifier*)item
    beforeWebStateIndex:(int)nextWebStateIndex {
  GridItemIdentifier* nextItem = nil;
  if (self.webStateList->ContainsIndex(nextWebStateIndex)) {
    const TabGroup* group =
        self.webStateList->GetGroupOfWebStateAt(nextWebStateIndex);
    if (group) {
      nextItem = [GridItemIdentifier groupIdentifier:group
                                    withWebStateList:self.webStateList];
    } else {
      nextItem = [GridItemIdentifier
          tabIdentifier:self.webStateList->GetWebStateAt(nextWebStateIndex)];
    }
  }

  [self.consumer moveItem:item beforeItem:nextItem];
}

- (void)updateConsumerItemForWebState:(web::WebState*)webState {
  WebStateList* webStateList = self.webStateList;
  int index = webStateList->GetIndexOfWebState(webState);
  const TabGroup* group = nullptr;
  if (webStateList->ContainsIndex(index)) {
    group = webStateList->GetGroupOfWebStateAt(index);
  }
  GridItemIdentifier* item;
  if (group) {
    item = [GridItemIdentifier groupIdentifier:group
                              withWebStateList:webStateList];
  } else {
    item = [GridItemIdentifier tabIdentifier:webState];
  }
  [self.consumer replaceItem:item withReplacementItem:item];
}

- (void)closeTabGroup:(const TabGroup*)group andDeleteGroup:(BOOL)deleteGroup {
  if (!group) {
    return;
  }
  [self.tabGridIdleStatusHandler
      tabGridDidPerformAction:TabGridActionType::kInPageAction];

  WebStateList* groupWebStateList = [self groupWebStateList:group];
  if (!groupWebStateList) {
    // The group has already been removed.
    return;
  }

  if (groupWebStateList != _webStateList) {
    // `group` is not in the set of groups of the `_webStateList`, so `group`
    // should be a search result from a different window. Since this item is not
    // from the current browser, no UI updates will be sent to the current grid.
    // Notify the current grid consumer about the change.
    CHECK(_modeHolder.mode == TabGridMode::kSearch, base::NotFatalUntil::M130);
    GridItemIdentifier* identifierToRemove =
        [GridItemIdentifier groupIdentifier:group
                           withWebStateList:groupWebStateList];
    [self.consumer removeItemWithIdentifier:identifierToRemove
                     selectedItemIdentifier:nil];
  }

  if (IsTabGroupSyncEnabled() && !deleteGroup) {
    [self showTabGroupSnackbarOrIPH:1];
    tab_groups::TabGroupSyncService* syncService =
        tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
            self.browser->GetBrowserState());
    tab_groups::utils::CloseTabGroupLocally(group, groupWebStateList,
                                            syncService);
  } else {
    // Using `CloseAllWebStatesInGroup` will result in calling the web state
    // list observers which will take care of updating the consumer.
    CloseAllWebStatesInGroup(*groupWebStateList, group,
                             WebStateList::CLOSE_USER_ACTION);
  }
}

- (void)ungroupTabGroup:(const TabGroup*)group {
  if (!group) {
    return;
  }
  [self.tabGridIdleStatusHandler
      tabGridDidPerformAction:TabGridActionType::kInPageAction];

  WebStateList* groupWebStateList = [self groupWebStateList:group];
  if (!groupWebStateList) {
    // The group has already been removed.
    return;
  }

  if (groupWebStateList != _webStateList) {
    // `group` is not in the set of groups of the `_webStateList`, so `group`
    // should be a search result from a different window. Since this item is not
    // from the current browser, no UI updates will be sent to the current grid.
    // Notify the current grid consumer about the change.
    CHECK(_modeHolder.mode == TabGridMode::kSearch, base::NotFatalUntil::M130);
    GridItemIdentifier* identifierToRemove =
        [GridItemIdentifier groupIdentifier:group
                           withWebStateList:groupWebStateList];
    [self.consumer removeItemWithIdentifier:identifierToRemove
                     selectedItemIdentifier:nil];
  }

  groupWebStateList->DeleteGroup(group);
}

- (BOOL)canHandleTabGroupDrop:(TabGroupInfo*)tabGroupInfo {
  return self.browserState->IsOffTheRecord() == tabGroupInfo.incognito;
}

- (void)recordExternalURLDropped {
  base::UmaHistogramEnumeration(kUmaGridViewDragOrigin, DragItemOrigin::kOther);
}

- (void)showTabGroupSnackbarOrIPH:(int)closedGroups {
  if (!IsTabGroupSyncEnabled() || closedGroups < 1) {
    return;
  }
  [self.tabGroupsHandler
      showTabGridTabGroupSnackbarAfterClosingGroups:closedGroups];
  [self.tabGridToolbarHandler showSavedTabGroupIPH];
}

#pragma mark - WebStateListObserving

- (void)willChangeWebStateList:(WebStateList*)webStateList
                        change:(const WebStateListChangeDetach&)detachChange
                        status:(const WebStateListStatus&)status {
  DCHECK_EQ(_webStateList, webStateList);
  if (webStateList->IsBatchInProgress()) {
    return;
  }

  // When the deleted tab is showing as an item (i.e. when it's not
  // grouped or shown as a search result), remove it from the grid.
  if (!detachChange.group() || _modeHolder.mode == TabGridMode::kSearch) {
    // Get the identifier to remove.
    web::WebState* detachedWebState = detachChange.detached_web_state();
    GridItemIdentifier* identifierToRemove =
        [GridItemIdentifier tabIdentifier:detachedWebState];

    // If the WebState is pinned and it is not in the consumer's items list,
    // consumer will filter it out in the method's implementation.
    [self.consumer removeItemWithIdentifier:identifierToRemove
                     selectedItemIdentifier:[self activeIdentifier]];
    [self removeFromSelectionItemID:identifierToRemove];
  }

  // The pinned WebState could be detached only in case it was displayed in
  // the Tab Search and was closed from the context menu. In such a case
  // there were no observation added for it. Therefore, there is no need to
  // remove one.
  if (![self isPinnedWebState:detachChange.detached_from_index()]) {
    [self removeObservationForWebState:detachChange.detached_web_state()];
  }
}

- (void)didChangeWebStateList:(WebStateList*)webStateList
                       change:(const WebStateListChange&)change
                       status:(const WebStateListStatus&)status {
  DCHECK_EQ(_webStateList, webStateList);
  if (webStateList->IsBatchInProgress()) {
    if (change.type() == WebStateListChange::Type::kInsert) {
      [self updateForTabInserted];
    }
    return;
  }

  switch (change.type()) {
    case WebStateListChange::Type::kStatusOnly: {
      const WebStateListChangeStatusOnly& selectionOnlyChange =
          change.As<WebStateListChangeStatusOnly>();
      if (selectionOnlyChange.pinned_state_changed()) {
        [self
            changePinnedStateForWebState:selectionOnlyChange.web_state()
                                 atIndex:selectionOnlyChange.index()
                            updatedGroup:selectionOnlyChange.old_group()
                                             ? selectionOnlyChange.old_group()
                                             : selectionOnlyChange.new_group()];
        break;
      }
      const TabGroup* oldGroup = selectionOnlyChange.old_group();
      const TabGroup* newGroup = selectionOnlyChange.new_group();

      if (oldGroup != newGroup) {
        // There is a change of group.
        if (oldGroup == nullptr) {
          // The tab was ungrouped and is moving to a group.
          web::WebState* currentWebState =
              _webStateList->GetWebStateAt(selectionOnlyChange.index());

          GridItemIdentifier* tabIdentifierToAddToGroup =
              [GridItemIdentifier tabIdentifier:currentWebState];

          [self.consumer removeItemWithIdentifier:tabIdentifierToAddToGroup
                           selectedItemIdentifier:[self activeIdentifier]];
        } else {
          // The tab left a group.
          GridItemIdentifier* oldGroupIdentifier =
              [GridItemIdentifier groupIdentifier:oldGroup
                                 withWebStateList:_webStateList];
          [self.consumer replaceItem:oldGroupIdentifier
                 withReplacementItem:oldGroupIdentifier];
        }

        if (newGroup) {
          // The tab joined a group.
          GridItemIdentifier* newGroupIdentifier =
              [GridItemIdentifier groupIdentifier:newGroup
                                 withWebStateList:_webStateList];

          [self.consumer replaceItem:newGroupIdentifier
                 withReplacementItem:newGroupIdentifier];
        } else {
          // The tab is now ungrouped.
          int webStateIndex = selectionOnlyChange.index();
          web::WebState* currentWebState =
              _webStateList->GetWebStateAt(webStateIndex);

          [self insertItem:[GridItemIdentifier tabIdentifier:currentWebState]
              beforeWebStateIndex:webStateIndex + 1];
        }

        // If the web state is the active one, the new group needs to be
        // highlighted.
        if (selectionOnlyChange.index() == webStateList->active_index()) {
          [self.consumer selectItemWithIdentifier:[self activeIdentifier]];
        }
        break;
      }
      // The activation is handled after this switch statement.
      break;
    }
    case WebStateListChange::Type::kDetach: {
      const WebStateListChangeDetach& detachChange =
          change.As<WebStateListChangeDetach>();
      if (detachChange.group()) {
        [self updateCellGroup:detachChange.group()];
      }
      // Do not manage other case scenarios as this is already handled in
      // `-willChangeWebStateList:change:status:` function.
      break;
    }
    case WebStateListChange::Type::kMove: {
      const WebStateListChangeMove& moveChange =
          change.As<WebStateListChangeMove>();

      if (moveChange.pinned_state_changed()) {
        // The pinned state can be updated when a tab is moved.
        [self changePinnedStateForWebState:moveChange.moved_web_state()
                                   atIndex:moveChange.moved_to_index()
                              updatedGroup:moveChange.old_group()
                                               ? moveChange.old_group()
                                               : moveChange.new_group()];
      } else if (![self isPinnedWebState:moveChange.moved_to_index()]) {
        // BaseGridMediator handles only non pinned tabs because pinned tabs are
        // handled in PinnedTabsMediator.
        if (moveChange.old_group() == moveChange.new_group()) {
          // No group update.
          if (moveChange.old_group()) {
            // The cell moved inside its group, update the group.
            [self updateCellGroup:moveChange.old_group()];
          } else {
            // The cell is not in a group, move it.
            [self moveItem:[GridItemIdentifier
                               tabIdentifier:moveChange.moved_web_state()]
                beforeWebStateIndex:moveChange.moved_to_index() + 1];
          }
        } else {
          // The group has changed.
          if (moveChange.old_group()) {
            // The cell left a group.
            [self updateCellGroup:moveChange.old_group()];
          } else {
            // The cell wasn't in a group, remove it from the grid.
            [self.consumer removeItemWithIdentifier:
                               [GridItemIdentifier
                                   tabIdentifier:moveChange.moved_web_state()]
                             selectedItemIdentifier:[self activeIdentifier]];
          }
          if (moveChange.new_group()) {
            // The cell joined a group.
            [self updateCellGroup:moveChange.new_group()];
          } else {
            // The cell was removed from its group, add it to the grid.
            web::WebState* movedWebState = moveChange.moved_web_state();
            [self insertItem:[GridItemIdentifier tabIdentifier:movedWebState]
                beforeWebStateIndex:moveChange.moved_to_index() + 1];
          }
          // If the web state is the active one, the new group needs to be
          // highlighted.
          if (moveChange.moved_to_index() == webStateList->active_index()) {
            [self.consumer selectItemWithIdentifier:[self activeIdentifier]];
          }
        }
      }
      break;
    }
    case WebStateListChange::Type::kReplace: {
      const WebStateListChangeReplace& replaceChange =
          change.As<WebStateListChangeReplace>();
      if ([self isPinnedWebState:replaceChange.index()]) {
        break;
      }
      web::WebState* replacedWebState = replaceChange.replaced_web_state();
      web::WebState* insertedWebState = replaceChange.inserted_web_state();
      [self.consumer replaceItem:[GridItemIdentifier
                                     tabIdentifier:replacedWebState]
             withReplacementItem:[GridItemIdentifier
                                     tabIdentifier:insertedWebState]];

      [self removeObservationForWebState:replacedWebState];
      [self addObservationForWebState:insertedWebState];
      break;
    }
    case WebStateListChange::Type::kInsert: {
      [self updateForTabInserted];
      const WebStateListChangeInsert& insertChange =
          change.As<WebStateListChangeInsert>();
      if ([self isPinnedWebState:insertChange.index()]) {
        [self.consumer selectItemWithIdentifier:[self activeIdentifier]];
        break;
      }

      if (insertChange.group()) {
        [self updateCellGroup:insertChange.group()];
      } else {
        web::WebState* insertedWebState = insertChange.inserted_web_state();
        [self insertItem:[GridItemIdentifier tabIdentifier:insertedWebState]
            beforeWebStateIndex:insertChange.index() + 1];
      }
      [self addObservationForWebState:insertChange.inserted_web_state()];
      break;
    }
    case WebStateListChange::Type::kGroupCreate: {
      const WebStateListChangeGroupCreate& groupCreateChange =
          change.As<WebStateListChangeGroupCreate>();

      const TabGroup* currentGroup = groupCreateChange.created_group();
      GridItemIdentifier* groupItemIdentifier =
          [GridItemIdentifier groupIdentifier:currentGroup
                             withWebStateList:webStateList];
      CHECK(groupItemIdentifier.tabGroupItem.tabGroup);
      [self insertItem:groupItemIdentifier
          beforeWebStateIndex:groupItemIdentifier.tabGroupItem.tabGroup->range()
                                  .range_end() +
                              1];
      break;
    }
    case WebStateListChange::Type::kGroupVisualDataUpdate: {
      const WebStateListChangeGroupVisualDataUpdate& visualDataChange =
          change.As<WebStateListChangeGroupVisualDataUpdate>();
      GridItemIdentifier* groupItemIdentifier =
          [GridItemIdentifier groupIdentifier:visualDataChange.updated_group()
                             withWebStateList:webStateList];
      [self.consumer replaceItem:groupItemIdentifier
             withReplacementItem:groupItemIdentifier];

      break;
    }
    case WebStateListChange::Type::kGroupMove: {
      const WebStateListChangeGroupMove& groupMoveChange =
          change.As<WebStateListChangeGroupMove>();
      [self moveItem:[GridItemIdentifier
                          groupIdentifier:groupMoveChange.moved_group()
                         withWebStateList:webStateList]
          beforeWebStateIndex:groupMoveChange.moved_to_range().range_end()];
      break;
    }
    case WebStateListChange::Type::kGroupDelete: {
      const WebStateListChangeGroupDelete& groupDeleteChange =
          change.As<WebStateListChangeGroupDelete>();

      GridItemIdentifier* groupItemIdentifier =
          [GridItemIdentifier groupIdentifier:groupDeleteChange.deleted_group()
                             withWebStateList:_webStateList];
      [_selectedEditingItems removeItem:groupItemIdentifier];
      [self.consumer removeItemWithIdentifier:groupItemIdentifier
                       selectedItemIdentifier:[self activeIdentifier]];
      break;
    }
  }
  [self updateToolbarAfterNumberOfItemsChanged];
  if (status.active_web_state_change()) {
    [self.consumer selectItemWithIdentifier:[self activeIdentifier]];
  }
}

- (void)webStateListWillBeginBatchOperation:(WebStateList*)webStateList {
  DCHECK_EQ(_webStateList, webStateList);
  _scopedWebStateObservation->RemoveAllObservations();
}

- (void)webStateListBatchOperationEnded:(WebStateList*)webStateList {
  DCHECK_EQ(_webStateList, webStateList);

  // Clear selections.
  [_selectedEditingItems removeAllItems];

  [self addWebStateObservations];
  [self populateConsumerItems];
  [self updateToolbarAfterNumberOfItemsChanged];
}

#pragma mark - CRWWebStateObserver

- (void)webStateDidStartLoading:(web::WebState*)webState {
  [self updateConsumerItemForWebState:webState];
}

- (void)webStateDidStopLoading:(web::WebState*)webState {
  [self updateConsumerItemForWebState:webState];
}

- (void)webStateDidChangeTitle:(web::WebState*)webState {
  [self updateConsumerItemForWebState:webState];
}

#pragma mark - TabGridModeObserving

- (void)tabGridModeDidChange:(TabGridModeHolder*)modeHolder {
  // Clear selections.
  [_selectedEditingItems removeAllItems];
  [self configureToolbarsButtons];
  [self.consumer setTabGridMode:modeHolder.mode];
}

#pragma mark - SnapshotStorageObserver

- (void)didUpdateSnapshotStorageWithSnapshotID:(SnapshotIDWrapper*)snapshotID {
  web::WebState* webState = nullptr;
  WebStateList* webStateList = self.webStateList;
  for (int i = webStateList->pinned_tabs_count(); i < webStateList->count();
       i++) {
    SnapshotTabHelper* snapshotTabHelper =
        SnapshotTabHelper::FromWebState(webStateList->GetWebStateAt(i));
    if (snapshotID.snapshot_id == snapshotTabHelper->GetSnapshotID()) {
      webState = webStateList->GetWebStateAt(i);
      break;
    }
  }
  if (webState) {
    // It is possible to observe an updated snapshot for a WebState before
    // observing that the WebState has been added to the WebStateList. It is the
    // consumer's responsibility to ignore any updates before inserts.
    [self updateConsumerItemForWebState:webState];
  }
}

#pragma mark - GridCommands

- (BOOL)addNewItem {
  // The incognito mediator's Browser is briefly set to nil after the last
  // incognito tab is closed.
  if (!self.browser) {
    return NO;
  }

  if (self.browserState &&
      !IsAddNewTabAllowedByPolicy(self.browserState->GetPrefs(),
                                  self.browserState->IsOffTheRecord())) {
    return NO;
  }

  // The function is clamping the value, so it safe to pass the total count of
  // the WebState even if it is supposed to be a grid index.
  [self insertNewWebStateAtGridIndex:self.webStateList->count()
                             withURL:GURL(kChromeUINewTabURL)];
  return YES;
}

- (void)selectItemWithID:(web::WebStateID)itemID
                    pinned:(BOOL)pinned
    isFirstActionOnTabGrid:(BOOL)isFirstActionOnTabGrid {
  WebStateSearchCriteria searchCriteria{
      .identifier = itemID,
      .pinned_state = pinned ? PinnedState::kPinned : PinnedState::kNonPinned,
  };

  int index = GetWebStateIndex(self.webStateList, searchCriteria);
  WebStateList* itemWebStateList = self.webStateList;
  if (index == WebStateList::kInvalidIndex) {
    if (pinned) {
      return;
    }
    // If this is a search result, it may contain items from other windows or
    // from the inactive browser - check inactive browser and other windows
    // before giving up.
    BrowserList* browserList =
        BrowserListFactory::GetForBrowserState(self.browserState);
    Browser* browser = GetBrowserForNonPinnedTabWithId(
        browserList, itemID, self.browserState->IsOffTheRecord());

    if (!browser) {
      return;
    }

    if (browser->IsInactive()) {
      base::RecordAction(
          base::UserMetricsAction("MobileTabGridOpenInactiveTabSearchResult"));
      index = itemWebStateList->count();
      MoveTabToBrowser(itemID, self.browser, index);
    } else {
      // Other windows case.
      itemWebStateList = browser->GetWebStateList();
      index = GetWebStateIndex(itemWebStateList,
                               WebStateSearchCriteria{
                                   .identifier = itemID,
                                   .pinned_state = PinnedState::kNonPinned,
                               });
      SceneState* targetSceneState = browser->GetSceneState();
      SceneState* currentSceneState = self.browser->GetSceneState();

      UISceneActivationRequestOptions* options =
          [[UISceneActivationRequestOptions alloc] init];
      options.requestingScene = currentSceneState.scene;

      [[UIApplication sharedApplication]
          requestSceneSessionActivation:targetSceneState.scene.session
                           userActivity:nil
                                options:options
                           errorHandler:^(NSError* error) {
                             LOG(ERROR) << base::SysNSStringToUTF8(
                                 error.localizedDescription);
                             NOTREACHED_IN_MIGRATION();
                           }];
    }
  }

  web::WebState* selectedWebState = itemWebStateList->GetWebStateAt(index);
  LogPriceDropMetrics(selectedWebState);

  base::TimeDelta timeSinceLastActivation =
      base::Time::Now() - selectedWebState->GetLastActiveTime();
  base::UmaHistogramCustomTimes(
      "IOS.TabGrid.TabSelected.TimeSinceLastActivation",
      timeSinceLastActivation, base::Minutes(1), base::Days(24), 50);

  // Don't attempt a no-op activation. Normally this is not an issue, but it's
  // possible that this method (-selectItemWithID:) is being called as part of
  // a WebStateListObserver callback, in which case even a no-op activation
  // will cause a CHECK().
  if (selectedWebState == itemWebStateList->GetActiveWebState()) {
    // In search mode, the consumer doesn't have any information about the
    // selected item. So even if the active WebState is the same as the one that
    // is being selected, make sure that the consumer updates its selected item.
    [self.consumer
        selectItemWithIdentifier:[GridItemIdentifier
                                     tabIdentifier:selectedWebState]];
    return;
  } else {
    base::RecordAction(
        base::UserMetricsAction("MobileTabGridMoveToExistingTab"));
    if (isFirstActionOnTabGrid) {
      int activeWebStateIndex = itemWebStateList->active_index();
      BOOL adjacentTabSelected =
          std::abs(index - activeWebStateIndex) == 1 &&
          index != WebStateList::kInvalidIndex &&
          activeWebStateIndex != WebStateList::kInvalidIndex;
      if (adjacentTabSelected && self.browser) {
        TabBasedIPHBrowserAgent* tabBasedIPHBrowserAgent =
            TabBasedIPHBrowserAgent::FromBrowser(self.browser);
        tabBasedIPHBrowserAgent->NotifySwitchToAdjacentTabFromTabGrid();
      }
    }
  }

  // Avoid a reentrant activation. This is a fix for crbug.com/1134663, although
  // ignoring the selection at this point may do weird things.
  if (itemWebStateList->IsMutating()) {
    return;
  }

  // It should be safe to activate here.
  itemWebStateList->ActivateWebStateAt(index);
}

- (void)selectTabGroup:(const TabGroup*)tabGroup {
  WebStateList* webStateList = self.webStateList;

  if (webStateList->ContainsGroup(tabGroup)) {
    [self.tabGroupsHandler showTabGroup:tabGroup];
    return;
  }

  BOOL incognito = self.browserState->IsOffTheRecord();
  // If this is a search result, it may contain items from other windows or
  // from the inactive browser - check other windows before giving up.
  BrowserList* browserList =
      BrowserListFactory::GetForBrowserState(self.browserState);
  Browser* browser = GetBrowserForGroup(browserList, tabGroup, incognito);

  if (!browser) {
    return;
  }

  base::RecordAction(
      base::UserMetricsAction("MobileTabGridOpenSearchResultInAnotherWindow"));

  SceneState* targetSceneState = browser->GetSceneState();
  SceneState* currentSceneState = self.browser->GetSceneState();

  UISceneActivationRequestOptions* options =
      [[UISceneActivationRequestOptions alloc] init];
  options.requestingScene = currentSceneState.scene;

  [[UIApplication sharedApplication]
      requestSceneSessionActivation:targetSceneState.scene.session
                       userActivity:nil
                            options:options
                       errorHandler:^(NSError* error) {
                         LOG(ERROR) << base::SysNSStringToUTF8(
                             error.localizedDescription);
                         NOTREACHED_IN_MIGRATION();
                       }];

  if (!targetSceneState.UIEnabled) {
    return;
  }

  id<ApplicationCommands> applicationHandler =
      HandlerForProtocol(browser->GetCommandDispatcher(), ApplicationCommands);
  TabGridOpeningMode openingMode =
      incognito ? TabGridOpeningMode::kIncognito : TabGridOpeningMode::kRegular;
  [applicationHandler displayTabGridInMode:openingMode];

  id<TabGroupsCommands> tabGroupsHandler =
      HandlerForProtocol(browser->GetCommandDispatcher(), TabGroupsCommands);
  [tabGroupsHandler showTabGroup:tabGroup];
}

- (BOOL)isItemWithIDSelected:(web::WebStateID)itemID {
  int index = GetWebStateIndex(self.webStateList,
                               WebStateSearchCriteria{.identifier = itemID});
  if (index == WebStateList::kInvalidIndex) {
    return NO;
  }
  return index == self.webStateList->active_index();
}

- (void)setPinState:(BOOL)pinState forItemWithID:(web::WebStateID)itemID {
  SetWebStatePinnedState(self.webStateList, itemID, pinState);
}

- (void)closeItemWithID:(web::WebStateID)itemID {
  [self.tabGridIdleStatusHandler
      tabGridDidPerformAction:TabGridActionType::kInPageAction];
  int index = GetWebStateIndex(self.webStateList,
                               WebStateSearchCriteria{
                                   .identifier = itemID,
                               });
  if (index != WebStateList::kInvalidIndex) {
    self.webStateList->CloseWebStateAt(index, WebStateList::CLOSE_USER_ACTION);
    return;
  }

  TabSwitcherItem* itemToRemove =
      [[TabSwitcherItem alloc] initWithIdentifier:itemID];

  GridItemIdentifier* identifierToRemove =
      [[GridItemIdentifier alloc] initWithTabItem:itemToRemove];

  // `index` is `WebStateList::kInvalidIndex`, so `itemID` should be a search
  // result from a different window. Since this item is not from the current
  // browser, no UI updates will be sent to the current grid. Notify the current
  // grid consumer about the change.
  [self.consumer removeItemWithIdentifier:identifierToRemove
                   selectedItemIdentifier:nil];
  base::RecordAction(
      base::UserMetricsAction("MobileTabGridSearchCloseTabFromAnotherWindow"));

  BrowserList* browserList =
      BrowserListFactory::GetForBrowserState(self.browserState);
  Browser* browser = GetBrowserForNonPinnedTabWithId(
      browserList, itemID, self.browserState->IsOffTheRecord());

  // If this tab is still associated with another browser, remove it from the
  // associated web state list.
  if (browser) {
    WebStateList* itemWebStateList = browser->GetWebStateList();
    index = GetWebStateIndex(itemWebStateList,
                             WebStateSearchCriteria{
                                 .identifier = itemID,
                                 .pinned_state = PinnedState::kNonPinned,
                             });
    itemWebStateList->CloseWebStateAt(index, WebStateList::CLOSE_USER_ACTION);
  }
}

- (void)closeItemsWithTabIDs:(const std::set<web::WebStateID>&)tabIDs
                    groupIDs:(const std::set<tab_groups::TabGroupId>&)groupIDs
                    tabCount:(int)tabCount {
  base::UmaHistogramCounts100("IOS.TabGrid.Selection.CloseTabs", tabCount);
  RecordTabGridCloseTabsCount(tabCount);

  WebStateList* webStateList = self.webStateList;
  int closedGroupsCount = groupIDs.size();

  if (IsTabGroupSyncEnabled() && closedGroupsCount > 0) {
    tab_groups::TabGroupSyncService* syncService =
        tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
            self.browser->GetBrowserState());

    // Find and close all groups in `groupIDs`.
    for (const TabGroup* group : webStateList->GetGroups()) {
      tab_groups::TabGroupId groupID = group->tab_group_id();
      if (groupIDs.contains(groupID)) {
        tab_groups::utils::CloseTabGroupLocally(group, webStateList,
                                                syncService);
      }
    }
  }

  {
    WebStateList::ScopedBatchOperation lock =
        webStateList->StartBatchOperation();
    for (const web::WebStateID itemID : tabIDs) {
      const int index = GetWebStateIndex(
          webStateList,
          WebStateSearchCriteria{.identifier = itemID,
                                 .pinned_state = PinnedState::kNonPinned});
      if (index != WebStateList::kInvalidIndex) {
        GridItemIdentifier* identifierToRemove = [GridItemIdentifier
            tabIdentifier:webStateList->GetWebStateAt(index)];
        [_selectedEditingItems removeItem:identifierToRemove];
        webStateList->CloseWebStateAt(index, WebStateList::CLOSE_USER_ACTION);
      }
    }
  }

  const bool allTabsClosed = webStateList->empty();
  if (allTabsClosed) {
    if (!self.browserState->IsOffTheRecord()) {
      base::RecordAction(base::UserMetricsAction(
          "MobileTabGridSelectionCloseAllRegularTabsConfirmed"));
    } else {
      base::RecordAction(base::UserMetricsAction(
          "MobileTabGridSelectionCloseAllIncognitoTabsConfirmed"));
    }
  }

  if (IsTabGroupSyncEnabled() && closedGroupsCount > 0) {
    [self showTabGroupSnackbarOrIPH:closedGroupsCount];
  }
}

- (void)deleteTabGroup:(base::WeakPtr<const TabGroup>)group
            sourceView:(UIView*)sourceView {
  if (IsTabGroupSyncEnabled()) {
    [self.tabGroupsHandler
        showTabGroupConfirmationForAction:TabGroupActionType::kDeleteTabGroup
                                    group:group
                               sourceView:sourceView];
    return;
  }

  DCHECK(!IsTabGroupSyncEnabled());
  [self closeTabGroup:group.get() andDeleteGroup:YES];
}

- (void)closeTabGroup:(base::WeakPtr<const TabGroup>)group {
  [self closeTabGroup:group.get() andDeleteGroup:NO];
}

- (void)ungroupTabGroup:(base::WeakPtr<const TabGroup>)group
             sourceView:(UIView*)sourceView {
  if (IsTabGroupSyncEnabled()) {
    [self.tabGroupsHandler
        showTabGroupConfirmationForAction:TabGroupActionType::kUngroupTabGroup
                                    group:group
                               sourceView:sourceView];
    return;
  }

  DCHECK(!IsTabGroupSyncEnabled());
  [self ungroupTabGroup:group.get()];
}

- (void)closeAllItems {
  NOTREACHED() << "Should be implemented in a subclass.";
}

- (void)saveAndCloseAllItems {
  NOTREACHED() << "Should be implemented in a subclass.";
}

- (void)undoCloseAllItems {
  NOTREACHED() << "Should be implemented in a subclass.";
}

- (void)discardSavedClosedItems {
  NOTREACHED() << "Should be implemented in a subclass.";
}

- (void)searchItemsWithText:(NSString*)searchText {
  TabsSearchService* searchService =
      TabsSearchServiceFactory::GetForBrowserState(self.browserState);
  const std::u16string& searchTerm = base::SysNSStringToUTF16(searchText);
  searchService->Search(
      searchTerm,
      base::BindOnce(^(
          std::vector<TabsSearchService::TabsSearchBrowserResults> results) {
        NSMutableArray* currentBrowserItems = [[NSMutableArray alloc] init];
        NSMutableArray* remainingItems = [[NSMutableArray alloc] init];
        for (const TabsSearchService::TabsSearchBrowserResults& browserResults :
             results) {
          if (IsTabGroupInGridEnabled()) {
            for (const TabGroup* group : browserResults.tab_groups) {
              GridItemIdentifier* item = [GridItemIdentifier
                   groupIdentifier:group
                  withWebStateList:browserResults.browser->GetWebStateList()];
              if (browserResults.browser == self.browser) {
                [currentBrowserItems addObject:item];
              } else {
                [remainingItems addObject:item];
              }
            }
          }

          for (web::WebState* webState : browserResults.web_states) {
            GridItemIdentifier* item =
                [GridItemIdentifier tabIdentifier:webState];

            if (browserResults.browser == self.browser) {
              [currentBrowserItems addObject:item];
            } else {
              [remainingItems addObject:item];
            }
          }
        }

        NSArray* allItems = nil;
        // If there are results from Browsers other than the current one,
        // append those results to the end.
        if (remainingItems.count) {
          allItems = [currentBrowserItems
              arrayByAddingObjectsFromArray:remainingItems];
        } else {
          allItems = currentBrowserItems;
        }

        [self.consumer populateItems:allItems selectedItemIdentifier:nil];
      }));
}

- (void)resetToAllItems {
  [self populateConsumerItems];
}

#pragma mark - SuggestedActionsDelegate

- (void)fetchSearchHistoryResultsCountForText:(NSString*)searchText
                                   completion:(void (^)(size_t))completion {
  CHECK(!self.browserState->IsOffTheRecord());
  TabsSearchService* search_service =
      TabsSearchServiceFactory::GetForBrowserState(self.browserState);
  const std::u16string& searchTerm = base::SysNSStringToUTF16(searchText);
  search_service->SearchHistory(searchTerm,
                                base::BindOnce(^(size_t resultCount) {
                                  completion(resultCount);
                                }));
}

#pragma mark - TabCollectionDragDropHandler

- (NSArray<UIDragItem*>*)allSelectedDragItems {
  NSMutableArray<UIDragItem*>* dragItems = [[NSMutableArray alloc] init];
  for (GridItemIdentifier* itemID in _selectedEditingItems.itemsIdentifiers) {
    switch (itemID.type) {
      case GridItemType::kInactiveTabsButton:
        // Inactive Tabs button is not dragable and not stored in
        // `_selectedEditingItems`.
        NOTREACHED();
      case GridItemType::kTab: {
        UIDragItem* dragItem =
            [self dragItemForItemWithID:itemID.tabSwitcherItem.identifier];
        if (dragItem) {
          [dragItems addObject:dragItem];
        }
        break;
      }
      case GridItemType::kGroup: {
        UIDragItem* dragItem =
            [self dragItemForTabGroupItem:itemID.tabGroupItem];
        if (dragItem) {
          [dragItems addObject:dragItem];
        }
        break;
      }
      case GridItemType::kSuggestedActions:
        // Suggested actions items are not dragable and not stored in
        // `_selectedEditingItems`.
        NOTREACHED();
    }
  }
  return dragItems;
}

- (UIDragItem*)dragItemForTabGroupItem:(TabGroupItem*)tabGroupItem {
  return CreateTabGroupDragItem(tabGroupItem.tabGroup, self.browserState);
}

- (UIDragItem*)dragItemForItem:(TabSwitcherItem*)item {
  return [self dragItemForItemWithID:item.identifier];
}

- (void)dragSessionDidEnd {
  // Update buttons as the number of items or the number of selected items might
  // have changed.
  [self.toolbarsMutator setButtonsEnabled:YES];
  [self configureToolbarsButtons];
}

- (UIDropOperation)dropOperationForDropSession:(id<UIDropSession>)session
                                       toIndex:(NSUInteger)destinationIndex {
  UIDragItem* dragItem = session.localDragSession.items.firstObject;

  // Tab move operations only originate from Chrome so a local object is used.
  // Local objects allow synchronous drops, whereas NSItemProvider only allows
  // asynchronous drops.
  if ([dragItem.localObject isKindOfClass:[TabInfo class]]) {
    TabInfo* tabInfo = static_cast<TabInfo*>(dragItem.localObject);
    if (tabInfo.browserState != self.browserState) {
      // Tabs from different profiles cannot be dropped.
      return UIDropOperationForbidden;
    }

    if (self.browserState->IsOffTheRecord() == tabInfo.incognito) {
      return UIDropOperationMove;
    }

    // Tabs of different profiles (regular/incognito) cannot be dropped.
    return UIDropOperationForbidden;
  }
  if ([dragItem.localObject isKindOfClass:[TabGroupInfo class]]) {
    TabGroupInfo* tabGroupInfo =
        static_cast<TabGroupInfo*>(dragItem.localObject);
    if (tabGroupInfo.browserState != self.browserState) {
      // Tabs from different profiles cannot be dropped.
      return UIDropOperationForbidden;
    }
    return [self canHandleTabGroupDrop:tabGroupInfo] ? UIDropOperationMove
                                                     : UIDropOperationForbidden;
  }

  // All URLs originating from Chrome create a new tab (as opposed to moving a
  // tab).
  if ([dragItem.localObject isKindOfClass:[NSURL class]]) {
    return UIDropOperationCopy;
  }

  // URLs are accepted when drags originate from outside Chrome.
  NSArray<NSString*>* acceptableTypes = @[ UTTypeURL.identifier ];
  if ([session hasItemsConformingToTypeIdentifiers:acceptableTypes]) {
    return UIDropOperationCopy;
  }

  // Other UTI types such as image data or file data cannot be dropped.
  return UIDropOperationForbidden;
}

- (void)dropItem:(UIDragItem*)dragItem
               toIndex:(NSUInteger)destinationIndex
    fromSameCollection:(BOOL)fromSameCollection {
  WebStateList* webStateList = self.webStateList;

  // Tab move operations only originate from Chrome so a local object is used.
  // Local objects allow synchronous drops, whereas NSItemProvider only allows
  // asynchronous drops.
  if ([dragItem.localObject isKindOfClass:[TabInfo class]]) {
    TabInfo* tabInfo = static_cast<TabInfo*>(dragItem.localObject);

    if (IsPinnedTabsEnabled()) {
      // Try to unpin the tab, if not pinned nothing happens.
      SetWebStatePinnedState(webStateList, tabInfo.tabID,
                             /*pin_state=*/false);
    }

    int sourceWebStateIndex =
        GetWebStateIndex(webStateList, WebStateSearchCriteria{
                                           .identifier = tabInfo.tabID,
                                       });

    if (sourceWebStateIndex == WebStateList::kInvalidIndex) {
      // Move tab across Browsers.
      base::UmaHistogramEnumeration(kUmaGridViewDragOrigin,
                                    DragItemOrigin::kOtherBrowser);
      int destinationWebStateIndex =
          WebStateIndexFromGridDropItemIndex(webStateList, destinationIndex);

      MoveTabToBrowser(tabInfo.tabID, self.browser, destinationWebStateIndex);
      return;
    }

    if (fromSameCollection) {
      base::UmaHistogramEnumeration(kUmaGridViewDragOrigin,
                                    DragItemOrigin::kSameCollection);
    } else {
      base::UmaHistogramEnumeration(kUmaGridViewDragOrigin,
                                    DragItemOrigin::kSameBrowser);
    }

    // Reorder tabs.
    int destinationWebStateIndex = WebStateIndexFromGridDropItemIndex(
        webStateList, destinationIndex, sourceWebStateIndex);
    const auto insertionParams =
        WebStateList::InsertionParams::AtIndex(destinationWebStateIndex);
    MoveWebStateWithIdentifierToInsertionParams(
        tabInfo.tabID, insertionParams, webStateList, fromSameCollection);
    return;
  }

  if ([dragItem.localObject isKindOfClass:[TabGroupInfo class]]) {
    TabGroupInfo* tabGroupInfo =
        static_cast<TabGroupInfo*>(dragItem.localObject);
    // Early return if the group has been closed during the drag an drop.
    if (!tabGroupInfo.tabGroup) {
      return;
    }
    if (fromSameCollection) {
      base::UmaHistogramEnumeration(kUmaGridViewDragOrigin,
                                    DragItemOrigin::kSameCollection);
      CHECK(tabGroupInfo.tabGroup);
      int sourceIndex = tabGroupInfo.tabGroup->range().range_begin();
      int nextWebStateIndex = WebStateIndexAfterGridDropItemIndex(
          webStateList, destinationIndex, sourceIndex);
      webStateList->MoveGroup(tabGroupInfo.tabGroup, nextWebStateIndex);
      return;
    } else {
      base::UmaHistogramEnumeration(kUmaGridViewDragOrigin,
                                    DragItemOrigin::kOtherBrowser);
    }

    int destinationWebStateIndex =
        WebStateIndexAfterGridDropItemIndex(webStateList, destinationIndex);
    tab_groups::utils::MoveTabGroupToBrowser(
        tabGroupInfo.tabGroup, self.browser, destinationWebStateIndex);
    return;
  }

  // Handle URLs from within Chrome synchronously using a local object.
  if ([dragItem.localObject isKindOfClass:[URLInfo class]]) {
    URLInfo* droppedURL = static_cast<URLInfo*>(dragItem.localObject);
    [self insertNewWebStateAtGridIndex:destinationIndex withURL:droppedURL.URL];
    base::UmaHistogramEnumeration(kUmaGridViewDragOrigin,
                                  DragItemOrigin::kOther);
    return;
  }
}

- (void)dropItemFromProvider:(NSItemProvider*)itemProvider
                     toIndex:(NSUInteger)destinationIndex
          placeholderContext:
              (id<UICollectionViewDropPlaceholderContext>)placeholderContext {
  if (![itemProvider canLoadObjectOfClass:[NSURL class]]) {
    [placeholderContext deletePlaceholder];
    return;
  }

  [self recordExternalURLDropped];

  __weak BaseGridMediator* weakSelf = self;
  auto loadHandler =
      ^(__kindof id<NSItemProviderReading> providedItem, NSError* error) {
        dispatch_async(dispatch_get_main_queue(), ^{
          [placeholderContext deletePlaceholder];
          NSURL* droppedURL = static_cast<NSURL*>(providedItem);
          [weakSelf
              insertNewWebStateAtGridIndex:destinationIndex
                                   withURL:net::GURLWithNSURL(droppedURL)];
        });
      };
  [itemProvider loadObjectOfClass:[NSURL class] completionHandler:loadHandler];
}

#pragma mark - Private

// Returns a SnapshotStorageWrapper for the current browser.
- (SnapshotStorageWrapper*)snapshotStorage {
  if (!self.browser) {
    return nil;
  }
  return SnapshotBrowserAgent::FromBrowser(self.browser)->snapshot_storage();
}

- (void)addItemsWithIDsToReadingList:(const std::set<web::WebStateID>&)itemIDs {
  [self.delegate dismissPopovers];

  base::UmaHistogramCounts100("IOS.TabGrid.Selection.AddToReadingList",
                              itemIDs.size());

  NSArray<URLWithTitle*>* URLs = [self urlsWithTitleFromItemIDs:itemIDs];

  ReadingListAddCommand* command =
      [[ReadingListAddCommand alloc] initWithURLs:URLs];
  ReadingListBrowserAgent* readingListBrowserAgent =
      ReadingListBrowserAgent::FromBrowser(self.browser);
  readingListBrowserAgent->AddURLsToReadingList(command.URLs);
}

- (void)addItemsWithIDsToBookmarks:(const std::set<web::WebStateID>&)itemIDs {
  id<BookmarksCommands> bookmarkHandler =
      HandlerForProtocol(_browser->GetCommandDispatcher(), BookmarksCommands);

  if (!bookmarkHandler) {
    return;
  }
  [self.delegate dismissPopovers];
  base::RecordAction(
      base::UserMetricsAction("MobileTabGridAddedMultipleNewBookmarks"));
  base::UmaHistogramCounts100("IOS.TabGrid.Selection.AddToBookmarks",
                              itemIDs.size());

  NSArray<URLWithTitle*>* URLs = [self urlsWithTitleFromItemIDs:itemIDs];

  [bookmarkHandler bookmarkWithFolderChooser:URLs];
}

- (NSArray<URLWithTitle*>*)urlsWithTitleFromItemIDs:
    (const std::set<web::WebStateID>&)itemIDs {
  NSMutableArray<URLWithTitle*>* URLs = [[NSMutableArray alloc] init];
  for (const web::WebStateID itemID : itemIDs) {
    TabItem* item = GetTabItem(self.webStateList,
                               WebStateSearchCriteria{
                                   .identifier = itemID,
                                   .pinned_state = PinnedState::kNonPinned,
                               });
    URLWithTitle* URL = [[URLWithTitle alloc] initWithURL:item.URL
                                                    title:item.title];
    [URLs addObject:URL];
  }
  return URLs;
}

// Inserts/removes a non pinned item to/from the collection.
- (void)changePinnedStateForWebState:(web::WebState*)webState
                             atIndex:(int)index
                        updatedGroup:(const TabGroup*)group {
  if ([self isPinnedWebState:index]) {
    if (group) {
      [self updateCellGroup:group];
    } else {
      GridItemIdentifier* identifierToRemove =
          [GridItemIdentifier tabIdentifier:webState];
      [self.consumer removeItemWithIdentifier:identifierToRemove
                       selectedItemIdentifier:[self activeIdentifier]];
    }
    [self removeObservationForWebState:webState];
  } else {
    if (group) {
      [self updateCellGroup:group];
    } else {
      [self insertItem:[GridItemIdentifier tabIdentifier:webState]
          beforeWebStateIndex:index + 1];
    }
    [self addObservationForWebState:webState];
  }
}

- (BOOL)isPinnedWebState:(int)index {
  if (IsPinnedTabsEnabled() && self.webStateList->IsWebStatePinnedAt(index)) {
    return YES;
  }
  return NO;
}

// Updates toolbars when the number of web state might be changed.
- (void)updateToolbarAfterNumberOfItemsChanged {
  if (_modeHolder.mode == TabGridMode::kSelection &&
      self.webStateList->empty()) {
    // Exit selection mode if there are no more tabs.
    _modeHolder.mode = TabGridMode::kNormal;
  } else {
    // Update toolbar's buttons as the number of tabs have probably changed so
    // the options changed (ex: "Undo" may be available now).
    [self configureToolbarsButtons];
  }
}

// Returns a drag item for the given `itemID`.
- (UIDragItem*)dragItemForItemWithID:(web::WebStateID)itemID {
  web::WebState* webState = GetWebState(
      self.webStateList, WebStateSearchCriteria{
                             .identifier = itemID,
                             .pinned_state = PinnedState::kNonPinned,
                         });
  return CreateTabDragItem(webState);
}

// Returns the menu to display when the Add To button is selected for `items`.
- (NSArray<UIMenuElement*>*)addToButtonMenuElements {
  if (!self.browser) {
    return nil;
  }

  NSMutableArray<UIMenuElement*>* actions = [[NSMutableArray alloc] init];

  ActionFactory* actionFactory = [[ActionFactory alloc]
      initWithScenario:kMenuScenarioHistogramTabGridAddTo];

  __weak BaseGridMediator* weakSelf = self;

  if (IsTabGroupInGridEnabled()) {
    auto addToGroupBlock = ^(const TabGroup* group) {
      [weakSelf addSelectedElementsToGroup:group];
    };
    UIMenuElement* addToGroup = [actionFactory
        menuToAddTabToGroupWithGroups:GetAllGroupsForBrowserState(_browserState)
                         numberOfTabs:_selectedEditingItems.tabsCount
                                block:addToGroupBlock];
    [actions addObject:[UIMenu menuWithTitle:@""
                                       image:nil
                                  identifier:nil
                                     options:UIMenuOptionsDisplayInline
                                    children:@[ addToGroup ]]];
  }

  // Copy the set of items, so that the following block can use it.
  std::set<web::WebStateID> shareableTabsCopy =
      [_selectedEditingItems sharableTabs];

  UIAction* addToReadingListAction =
      [actionFactory actionToAddToReadingListWithBlock:^{
        [weakSelf addItemsWithIDsToReadingList:shareableTabsCopy];
      }];

  UIAction* bookmarkAction = [actionFactory actionToBookmarkWithBlock:^{
    [weakSelf addItemsWithIDsToBookmarks:shareableTabsCopy];
  }];
  // Bookmarking can be disabled from prefs (from an enterprise policy),
  // if that's the case grey out the option in the menu.
  BOOL isEditBookmarksEnabled =
      self.browser->GetBrowserState()->GetPrefs()->GetBoolean(
          bookmarks::prefs::kEditBookmarksEnabled);
  if (!isEditBookmarksEnabled) {
    bookmarkAction.attributes = UIMenuElementAttributesDisabled;
  }
  if (shareableTabsCopy.size() == 0) {
    addToReadingListAction.attributes = UIMenuElementAttributesDisabled;
    bookmarkAction.attributes = UIMenuElementAttributesDisabled;
  }

  [actions addObject:addToReadingListAction];
  [actions addObject:bookmarkAction];

  return actions;
}

// Adds all the current selected elements to `group`. Pass nullptr to add to a
// new group.
- (void)addSelectedElementsToGroup:(const TabGroup*)group {
  std::set<web::WebStateID> selectedTabs = [_selectedEditingItems allTabs];
  if (group == nullptr) {
    [self.tabGroupsHandler showTabGroupCreationForTabs:selectedTabs];
  } else {
    WebStateList::ScopedBatchOperation lock =
        self.webStateList->StartBatchOperation();
    for (web::WebStateID webStateID : selectedTabs) {
      MoveTabToGroup(webStateID, group, _browserState);
    }
  }
}

// Returns the associated WebStateList for the given `group`.
- (WebStateList*)groupWebStateList:(const TabGroup*)group {
  if (_webStateList->ContainsGroup(group)) {
    return _webStateList;
  }
  BrowserList* browserList =
      BrowserListFactory::GetForBrowserState(self.browserState);
  Browser* browser = GetBrowserForGroup(browserList, group,
                                        self.browserState->IsOffTheRecord());
  if (!browser) {
    return nullptr;
  }
  return browser->GetWebStateList();
}

// Updates the cell of the given `group`.
- (void)updateCellGroup:(const TabGroup*)group {
  GridItemIdentifier* groupIdentifier =
      [GridItemIdentifier groupIdentifier:group
                         withWebStateList:self.webStateList];
  [self.consumer replaceItem:groupIdentifier
         withReplacementItem:groupIdentifier];
}

#pragma mark - TabGridPageMutator

- (void)currentlySelectedGrid:(BOOL)selected {
  NOTREACHED() << "Should be implemented in a subclass.";
}

- (void)setPageAsActive {
  NOTREACHED() << "Should be implemented in a subclass.";
}

#pragma mark - TabGridToolbarsGridDelegate

- (void)closeAllButtonTapped:(id)sender {
  NOTREACHED() << "Should be implemented in a subclass.";
}

- (void)doneButtonTapped:(id)sender {
  // Tapping Done when in selection mode, should only return back to the normal
  // mode.
  if (_modeHolder.mode == TabGridMode::kSelection) {
    _modeHolder.mode = TabGridMode::kNormal;
    // Records action when user exit the selection mode.
    base::RecordAction(base::UserMetricsAction("MobileTabGridSelectionDone"));
  } else {
    base::RecordAction(base::UserMetricsAction("MobileTabGridDone"));
    [self.tabGridHandler exitTabGrid];
  }
}

- (void)newTabButtonTapped:(id)sender {
  NOTREACHED() << "Should be implemented in a subclass.";
}

- (void)selectAllButtonTapped:(id)sender {
  NSUInteger selectedItemsCount = _selectedEditingItems.tabsCount;
  BOOL allItemsSelected =
      static_cast<int>(selectedItemsCount) ==
      (self.webStateList->count() - self.webStateList->pinned_tabs_count());

  // Deselect all items if they are all already selected.
  if (allItemsSelected) {
    [_selectedEditingItems removeAllItems];
    base::RecordAction(
        base::UserMetricsAction("MobileTabGridSelectionDeselectAll"));
  } else {
    NSArray<GridItemIdentifier*>* identifiers = CreateItems(self.webStateList);
    for (GridItemIdentifier* identifier in identifiers) {
      [self addToSelectionItemID:identifier];
    }
    base::RecordAction(
        base::UserMetricsAction("MobileTabGridSelectionSelectAll"));
  }
  [self.consumer reload];
  [self configureToolbarsButtons];
}

- (void)searchButtonTapped:(id)sender {
  base::RecordAction(base::UserMetricsAction("MobileTabGridSearchTabs"));
  _modeHolder.mode = TabGridMode::kSearch;
}

- (void)cancelSearchButtonTapped:(id)sender {
  base::RecordAction(base::UserMetricsAction("MobileTabGridCancelSearchTabs"));
  _modeHolder.mode = TabGridMode::kNormal;
}

- (void)closeSelectedTabs:(id)sender {
  [self.delegate dismissPopovers];

  std::set<web::WebStateID> selectedTabIDs;
  std::set<tab_groups::TabGroupId> selectedGroupIDs;
  int tabCount = 0;

  for (GridItemIdentifier* identifier in _selectedEditingItems
           .itemsIdentifiers) {
    switch (identifier.type) {
      case GridItemType::kInactiveTabsButton:
        NOTREACHED();
      case GridItemType::kTab: {
        selectedTabIDs.insert(identifier.tabSwitcherItem.identifier);
        tabCount++;
        break;
      }
      case GridItemType::kGroup: {
        CHECK(identifier.tabGroupItem.tabGroup);
        const TabGroup* group = identifier.tabGroupItem.tabGroup;
        selectedGroupIDs.insert(group->tab_group_id());
        tabCount += group->range().count();
        break;
      }
      case GridItemType::kSuggestedActions:
        NOTREACHED();
    }
  }

  [self.delegate baseGridMediator:self
      showCloseConfirmationWithTabIDs:selectedTabIDs
                             groupIDs:selectedGroupIDs
                             tabCount:tabCount
                               anchor:sender];
}

- (void)shareSelectedTabs:(id)sender {
  [self.delegate dismissPopovers];

  base::RecordAction(
      base::UserMetricsAction("MobileTabGridSelectionShareTabs"));
  base::UmaHistogramCounts100("IOS.TabGrid.Selection.ShareTabs",
                              _selectedEditingItems.sharableTabsCount);
  [self.delegate baseGridMediator:self
                        shareURLs:[_selectedEditingItems selectedTabsURLs]
                           anchor:sender];
}

- (void)selectTabsButtonTapped:(id)sender {
  base::RecordAction(base::UserMetricsAction("MobileTabGridSelectTabs"));
  _modeHolder.mode = TabGridMode::kSelection;
}

#pragma mark - GridViewControllerMutator

- (void)userTappedOnItemID:(GridItemIdentifier*)itemID {
  CHECK(itemID.type == GridItemType::kInactiveTabsButton ||
        itemID.type == GridItemType::kGroup ||
        itemID.type == GridItemType::kTab);
  if (_modeHolder.mode == TabGridMode::kSelection) {
    CHECK(itemID.type != GridItemType::kInactiveTabsButton);
    if ([self isItemSelected:itemID]) {
      [self removeFromSelectionItemID:itemID];
    } else {
      [self addToSelectionItemID:itemID];
    }
  }
}

- (void)addToSelectionItemID:(GridItemIdentifier*)itemID {
  CHECK(itemID.type == GridItemType::kTab ||
        itemID.type == GridItemType::kGroup);
  if (_modeHolder.mode != TabGridMode::kSelection) {
    base::debug::DumpWithoutCrashing();
    return;
  }
  [_selectedEditingItems addItem:itemID];
  [self configureToolbarsButtons];
}

- (void)removeFromSelectionItemID:(GridItemIdentifier*)itemID {
  CHECK(itemID.type == GridItemType::kTab ||
        itemID.type == GridItemType::kGroup);
  if (_modeHolder.mode != TabGridMode::kSelection) {
    return;
  }

  [_selectedEditingItems removeItem:itemID];
  [self configureToolbarsButtons];
}

- (void)closeItemWithIdentifier:(GridItemIdentifier*)identifier {
  switch (identifier.type) {
    case GridItemType::kInactiveTabsButton:
      NOTREACHED();
    case GridItemType::kTab:
      [self closeItemWithID:identifier.tabSwitcherItem.identifier];
      break;
    case GridItemType::kGroup: {
      const TabGroup* group = identifier.tabGroupItem.tabGroup;
      [self closeTabGroup:group andDeleteGroup:NO];
      break;
    }
    case GridItemType::kSuggestedActions:
      NOTREACHED();
  }
}

#pragma mark - BaseGridMediatorItemProvider

- (BOOL)isItemSelected:(GridItemIdentifier*)itemID {
  return [_selectedEditingItems containItem:itemID];
}

@end