chromium/ios/chrome/browser/ui/tab_switcher/tab_strip/coordinator/tab_strip_mediator.mm

// Copyright 2020 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_strip/coordinator/tab_strip_mediator.h"

#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

#import "base/apple/foundation_util.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "components/favicon/ios/web_favicon_driver.h"
#import "components/saved_tab_groups/saved_tab_group.h"
#import "components/saved_tab_groups/tab_group_sync_service.h"
#import "components/tab_groups/tab_group_color.h"
#import "components/tab_groups/tab_group_visual_data.h"
#import "ios/chrome/browser/drag_and_drop/model/drag_item_util.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_util.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/saved_tab_groups/model/ios_tab_group_sync_util.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/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/all_web_state_observation_forwarder.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_list_observer_bridge.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/chrome/browser/shared/public/commands/tab_strip_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_strip_last_tab_dragged_alert_command.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_drag_drop_metrics.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_strip/coordinator/tab_strip_mediator_utils.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_strip/ui/swift.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_strip/ui/tab_strip_features_utils.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/chrome/browser/web_state_list/model/web_state_list_favicon_driver_observer.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"

namespace {

// Finds any TabGroup in `web_state_list` whose range starts at `index`.
// Returns `nullptr` if no such TabGroup exists.
const TabGroup* FindTabGroupStartingAtIndex(int index,
                                            WebStateList* web_state_list) {
  CHECK(web_state_list);
  for (const TabGroup* group : web_state_list->GetGroups()) {
    if (group->range().range_begin() == index) {
      return group;
    }
  }
  return nullptr;
}

// Returns the `TabStripItemData` for a tab item at `index` in `web_state_list`.
TabStripItemData* CreateTabItemData(int index, WebStateList* web_state_list) {
  CHECK(web_state_list);
  CHECK(web_state_list->ContainsIndex(index), base::NotFatalUntil::M128);
  const TabGroup* group = web_state_list->GetGroupOfWebStateAt(index);
  TabStripItemData* data = [[TabStripItemData alloc] init];
  if (group) {
    const TabGroupRange range = group->range();
    data.isFirstTabInGroup = range.range_begin() == index;
    data.isLastTabInGroup = range.range_end() == index + 1;
    data.groupStrokeColor = group->GetColor();
  }
  return data;
}

// Returns the `TabStripItemData` for `group`.
TabStripItemData* CreateGroupItemData(const TabGroup* group) {
  TabStripItemData* data = [[TabStripItemData alloc] init];
  data.groupStrokeColor = group->GetColor();
  return data;
}

// Returns the `TabStripItemData` elements for WebStates and TabGroups in
// `range` in `web_state_list`. If `including_groups` is set to false, then
// TabGroups are not included in the result.
NSMutableArray<TabStripItemData*>* CreateItemData(
    WebStateList* web_state_list,
    bool including_hidden_tab_items = true,
    bool including_group_items = true,
    TabGroupRange range = TabGroupRange::InvalidRange()) {
  CHECK(web_state_list);
  if (!range.valid()) {
    range = {0, web_state_list->count()};
  }
  CHECK_GE(range.range_begin(), 0);
  CHECK_LE(range.range_end(), web_state_list->count());
  NSMutableArray<TabStripItemData*>* data = [[NSMutableArray alloc] init];
  for (int index : range) {
    const TabGroup* group_of_web_state = nullptr;
    if ([TabStripFeaturesUtils isModernTabStripWithTabGroups]) {
      CHECK(web_state_list->ContainsIndex(index), base::NotFatalUntil::M128);
      group_of_web_state = web_state_list->GetGroupOfWebStateAt(index);
      if (including_group_items) {
        const TabGroup* group_starting_at_index =
            FindTabGroupStartingAtIndex(index, web_state_list);
        if (group_starting_at_index) {
          [data addObject:CreateGroupItemData(group_starting_at_index)];
        }
      }
    }
    // The tab associated with WebState at `index` should be included in the
    // output if it has no group, or its group is not collapsed, or
    // `including_hidden_tab_items` is true.
    const bool should_include_tab_item =
        !group_of_web_state ||
        !group_of_web_state->visual_data().is_collapsed() ||
        including_hidden_tab_items;
    if (should_include_tab_item) {
      [data addObject:CreateTabItemData(index, web_state_list)];
    }
  }
  return data;
}

// Returns the `TabStripItemIdentifier` elements for WebStates and TabGroups in
// `range` in `web_state_list`. If `including_groups` is set to false, then
// TabGroups are not included in the result.
NSMutableArray<TabStripItemIdentifier*>* CreateItemIdentifiers(
    WebStateList* web_state_list,
    bool including_hidden_tab_items = true,
    bool including_group_items = true,
    TabGroupRange range = TabGroupRange::InvalidRange()) {
  CHECK(web_state_list);
  if (!range.valid()) {
    range = {0, web_state_list->count()};
  }
  CHECK_GE(range.range_begin(), 0);
  CHECK_LE(range.range_end(), web_state_list->count());
  NSMutableArray<TabStripItemIdentifier*>* item_identifiers =
      [[NSMutableArray alloc] init];
  for (int index : range) {
    const TabGroup* group_of_web_state = nullptr;
    if ([TabStripFeaturesUtils isModernTabStripWithTabGroups]) {
      CHECK(web_state_list->ContainsIndex(index), base::NotFatalUntil::M128);
      group_of_web_state = web_state_list->GetGroupOfWebStateAt(index);
      if (including_group_items) {
        const TabGroup* group_starting_at_index =
            FindTabGroupStartingAtIndex(index, web_state_list);
        if (group_starting_at_index) {
          [item_identifiers
              addObject:CreateGroupItemIdentifier(group_starting_at_index,
                                                  web_state_list)];
        }
      }
    }
    // The tab associated with WebState at `index` should be included in the
    // output if it has no group, or its group is not collapsed, or
    // `including_hidden_tab_items` is true.
    const bool should_include_tab_item =
        !group_of_web_state ||
        !group_of_web_state->visual_data().is_collapsed() ||
        including_hidden_tab_items;
    if (should_include_tab_item) {
      web::WebState* web_state = web_state_list->GetWebStateAt(index);
      [item_identifiers addObject:CreateTabItemIdentifier(web_state)];
    }
  }
  return item_identifiers;
}

}  // namespace

@interface TabStripMediator () <CRWWebStateObserver,
                                WebStateFaviconDriverObserver,
                                WebStateListObserving>
// The consumer for this object.
@property(nonatomic, weak) id<TabStripConsumer> consumer;

@end

@implementation TabStripMediator {
  // Bridge C++ WebStateListObserver methods to this TabStripController.
  std::unique_ptr<WebStateListObserverBridge> _webStateListObserver;
  // Bridge C++ WebStateObserver methods to this TabStripController.
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserver;
  // Forward observer methods for all WebStates in the WebStateList monitored
  // by the TabStripMediator.
  std::unique_ptr<AllWebStateObservationForwarder>
      _allWebStateObservationForwarder;
  // Bridges FaviconDriverObservers methods to this mediator, and maintains a
  // FaviconObserver for each all webstates.
  std::unique_ptr<WebStateListFaviconDriverObserver>
      _webStateListFaviconObserver;
  // Browser list.
  BrowserList* _browserList;

  // List of items in the tab strip when a drag operation starts.
  // Should be set back to `nil` when the drag operation ends.
  NSMutableArray<TabStripItemIdentifier*>* _dragItems;

  // Used to get info about saved groups and to mutate them.
  raw_ptr<tab_groups::TabGroupSyncService> _tabGroupSyncService;
}

- (instancetype)initWithConsumer:(id<TabStripConsumer>)consumer
             tabGroupSyncService:
                 (tab_groups::TabGroupSyncService*)tabGroupSyncService
                     browserList:(BrowserList*)browserList {
  if ((self = [super init])) {
    CHECK(browserList);
    _browserList = browserList;
    _tabGroupSyncService = tabGroupSyncService;
    _consumer = consumer;
  }
  return self;
}

#pragma mark - Public methods

- (void)disconnect {
  if (_webStateList) {
    [self removeWebStateObservations];
    _webStateListFaviconObserver.reset();
    _webStateList->RemoveObserver(_webStateListObserver.get());
    _webStateListObserver = nullptr;
    _webStateList = nullptr;
  }
  _tabStripHandler = nil;
  _browserList = nullptr;
}

- (void)cancelMoveForTab:(web::WebStateID)tabID
           originBrowser:(Browser*)originBrowser
             originIndex:(int)originIndex
              visualData:(const tab_groups::TabGroupVisualData&)visualData
            localGroupID:(const tab_groups::TabGroupId&)localGroupID
                 savedID:(const base::Uuid&)savedID {
  BrowserAndIndex browserAndIndex = FindBrowserAndIndex(
      tabID, _browserList->BrowsersOfType(BrowserList::BrowserType::kRegular));
  if (!browserAndIndex.browser || !originBrowser) {
    return;
  }

  originIndex =
      std::min(originIndex, originBrowser->GetWebStateList()->count() - 1);
  if (!originBrowser->GetWebStateList()->ContainsIndex(originIndex)) {
    return;
  }

  if (!_tabGroupSyncService) {
    return;
  }
  std::optional<tab_groups::SavedTabGroup> savedGroup =
      _tabGroupSyncService->GetGroup(savedID);
  if (!savedGroup || savedGroup->local_group_id() ||
      savedGroup->saved_tabs().size() != 1) {
    // Don't cancel if the saved group has been modified (deleted, associated
    // with another local group or changed its tabs).
    return;
  }

  const WebStateList::InsertionParams insertionParams =
      WebStateList::InsertionParams::AtIndex(originIndex);
  MoveTabToBrowser(tabID, originBrowser, insertionParams);

  // Move to the new group
  BrowserAndIndex browserAndIndexAfterMove = FindBrowserAndIndex(
      tabID, _browserList->BrowsersOfType(BrowserList::BrowserType::kRegular));
  if (!browserAndIndexAfterMove.browser) {
    return;
  }
  WebStateList* afterMoveWebStateList =
      browserAndIndexAfterMove.browser->GetWebStateList();
  const int afterMoveIndex = browserAndIndexAfterMove.tab_index;

  const web::WebState* const webState =
      afterMoveWebStateList->GetWebStateAt(afterMoveIndex);
  const std::u16string title = webState->GetTitle();
  const GURL url = webState->GetVisibleURL();

  {
    // As this is a "undo" the usual mechanisms should be paused as the tab
    // moved back to its original position shouldn't be treated as a "new" tab
    // added to the group.
    auto localObservationPauser =
        _tabGroupSyncService->CreateScopedLocalObserverPauser();

    afterMoveWebStateList->CreateGroup({afterMoveIndex}, visualData,
                                       localGroupID);
    _tabGroupSyncService->UpdateLocalTabGroupMapping(savedID, localGroupID);

    // In case the tab has changed (URL or title), update it.
    _tabGroupSyncService->UpdateLocalTabId(
        localGroupID, savedGroup->saved_tabs()[0].saved_tab_guid(),
        tabID.identifier());

    tab_groups::SavedTabGroupTabBuilder tab_builder;
    tab_builder.SetURL(url);
    tab_builder.SetTitle(title);
    _tabGroupSyncService->UpdateTab(localGroupID, tabID.identifier(),
                                    std::move(tab_builder));
  }
}

- (void)deleteSavedGroupWithID:(const base::Uuid&)savedID {
  _tabGroupSyncService->RemoveGroup(savedID);
}

- (void)ungroupGroup:(TabGroupItem*)tabGroupItem {
  if (!self.webStateList || !tabGroupItem.tabGroup) {
    return;
  }
  base::RecordAction(base::UserMetricsAction("MobileTabStripUngroupTabs"));
  self.webStateList->DeleteGroup(tabGroupItem.tabGroup);
}

- (void)deleteGroup:(TabGroupItem*)tabGroupItem {
  if (!self.webStateList || !tabGroupItem.tabGroup) {
    return;
  }
  base::RecordAction(base::UserMetricsAction("MobileTabStripDeleteGroup"));
  CloseAllWebStatesInGroup(*_webStateList, tabGroupItem.tabGroup,
                           WebStateList::CLOSE_USER_ACTION);
}

#pragma mark - Public properties

- (void)setWebStateList:(WebStateList*)webStateList {
  if (_webStateList) {
    [self removeWebStateObservations];
    _webStateListFaviconObserver.reset();
    _webStateList->RemoveObserver(_webStateListObserver.get());
  }

  _webStateList = webStateList;

  if (_webStateList) {
    DCHECK_GE(_webStateList->count(), 0);
    _webStateListObserver = std::make_unique<WebStateListObserverBridge>(self);
    _webStateList->AddObserver(_webStateListObserver.get());

    _webStateListFaviconObserver =
        std::make_unique<WebStateListFaviconDriverObserver>(_webStateList,
                                                            self);

    _webStateObserver = std::make_unique<web::WebStateObserverBridge>(self);
    [self addWebStateObservations];
  }

  [self populateConsumerItems];
}

#pragma mark - WebStateListObserving

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

  bool activeWebStateDidChangeStatus = false;
  bool activeWebStateDidMove = false;

  switch (change.type()) {
    case WebStateListChange::Type::kStatusOnly: {
      // The activation is handled after this switch statement.
      if (!status.active_web_state_change()) {
        const WebStateListChangeStatusOnly& statusOnlyChange =
            change.As<WebStateListChangeStatusOnly>();
        [self moveItemForWebState:statusOnlyChange.web_state()
            fromWebStateListIndex:statusOnlyChange.index()
              toWebStateListIndex:statusOnlyChange.index()
                         oldGroup:statusOnlyChange.old_group()
                         newGroup:statusOnlyChange.new_group()];
        if (statusOnlyChange.index() == webStateList->active_index()) {
          activeWebStateDidChangeStatus = true;
        }
      }
      break;
    }
    case WebStateListChange::Type::kDetach: {
      const WebStateListChangeDetach& detachChange =
          change.As<WebStateListChangeDetach>();
      web::WebState* detachedWebState = detachChange.detached_web_state();
      TabStripItemIdentifier* item = [TabStripItemIdentifier
          tabIdentifier:[[WebStateTabSwitcherItem alloc]
                            initWithWebState:detachedWebState]];
      [self.consumer removeItems:@[ item ]];
      // Reconfigure the group items if needed.
      const TabGroup* group = detachChange.group();
      if (group) {
        [self updateDataAndReconfigureItemsInGroup:group];
      }
      break;
    }
    case WebStateListChange::Type::kInsert: {
      const WebStateListChangeInsert& insertChange =
          change.As<WebStateListChangeInsert>();
      const int index = insertChange.index();
      web::WebState* insertedWebState = insertChange.inserted_web_state();
      TabStripItemIdentifier* itemIdentifier =
          CreateTabItemIdentifier(insertedWebState);
      TabStripItemData* itemData = CreateTabItemData(index, webStateList);
      [self.consumer updateItemData:@{itemIdentifier : itemData}
                   reconfigureItems:NO];
      const TabGroup* group = insertChange.group();

      if (TabStripItemIdentifier* previousItemIdentifier =
              [self destinationItemAtIndex:(index - 1) parentGroup:group]) {
        // If after the update, there is a neighbor WebState to the left of the
        // newly inserted WebState, then there are three cases where
        // `previousItemIdentifier` is not nil.
        // 1. `group` is not nil and the neighbor is in `group` too
        // (`previousItemIdentifier` is the neighbor WebState).
        // 2. `group` is nil and the neighbor is not in a group either
        // (`previousItemIdentifier` is the neighbor WebState).
        // 3. `group` is nil and the neighbor is in a different group
        // (`previousItemIdentifier` is the neighbor WebState's group).
        [self.consumer insertItems:@[ itemIdentifier ]
                         afterItem:previousItemIdentifier];
      } else if (TabStripItemIdentifier* nextItemIdentifier =
                     [self destinationItemAtIndex:(index + 1)
                                      parentGroup:group]) {
        // If after the update, there is a neighbor WebState to the right of the
        // newly inserted WebState, then there are the same three cases where
        // `nextItemIdentifier` is not nil.
        [self.consumer insertItems:@[ itemIdentifier ]
                        beforeItem:nextItemIdentifier];
      } else if (group) {
        // If there is no neighbor WebState in `group` to insert before/after,
        // then the item will be the first child of that group.
        TabGroupItem* groupItem =
            [[TabGroupItem alloc] initWithTabGroup:group
                                      webStateList:webStateList];
        [self.consumer insertItems:@[ itemIdentifier ] insideGroup:groupItem];
      } else if (const TabGroup* emptyGroupAtIndexZero =
                     FindTabGroupStartingAtIndex(0, _webStateList)) {
        // If `group` is null but there is no neighbor WebState to insert
        // before/after, then the WebStateList has no WebStates and this new
        // WebState is inserted at index 0. If there is an empty group at index
        // 0, the item should be inserted after that group.
        TabStripItemIdentifier* groupItemIdentifier =
            CreateGroupItemIdentifier(emptyGroupAtIndexZero, _webStateList);
        [self.consumer insertItems:@[ itemIdentifier ]
                         afterItem:groupItemIdentifier];
      } else {
        // If `group` is null, there are no WebStates in the WebStateList and
        // there is no empty group at index 0, then the new item should be
        // inserted at the beginning of the collection view.
        [self.consumer insertItems:@[ itemIdentifier ] afterItem:nil];
      }

      // Reconfigure the group items if needed.
      if (group) {
        [self updateDataAndReconfigureItemsInGroup:group];
      }
      break;
    }
    case WebStateListChange::Type::kMove: {
      const WebStateListChangeMove& moveChange =
          change.As<WebStateListChangeMove>();
      [self moveItemForWebState:moveChange.moved_web_state()
          fromWebStateListIndex:moveChange.moved_from_index()
            toWebStateListIndex:moveChange.moved_to_index()
                       oldGroup:moveChange.old_group()
                       newGroup:moveChange.new_group()];
      if (moveChange.moved_to_index() == webStateList->active_index()) {
        activeWebStateDidMove = true;
      }
      break;
    }
    case WebStateListChange::Type::kReplace: {
      const WebStateListChangeReplace& replaceChange =
          change.As<WebStateListChangeReplace>();
      const int index = replaceChange.index();
      TabSwitcherItem* oldItem = [[WebStateTabSwitcherItem alloc]
          initWithWebState:replaceChange.replaced_web_state()];
      web::WebState* newWebState = replaceChange.inserted_web_state();
      TabSwitcherItem* newItem =
          [[WebStateTabSwitcherItem alloc] initWithWebState:newWebState];
      TabStripItemIdentifier* newItemIdentifier =
          [TabStripItemIdentifier tabIdentifier:newItem];
      TabStripItemData* newItemData = CreateTabItemData(index, webStateList);
      [self.consumer updateItemData:@{newItemIdentifier : newItemData}
                   reconfigureItems:NO];
      [self.consumer replaceItem:oldItem withItem:newItem];
      break;
    }
    case WebStateListChange::Type::kGroupCreate: {
      const WebStateListChangeGroupCreate& groupCreateChange =
          change.As<WebStateListChangeGroupCreate>();
      const TabGroup* group = groupCreateChange.created_group();
      TabStripItemIdentifier* groupItemIdentifier =
          CreateGroupItemIdentifier(group, webStateList);
      TabStripItemData* groupItemData = CreateGroupItemData(group);
      [self.consumer updateItemData:@{groupItemIdentifier : groupItemData}
                   reconfigureItems:NO];
      // Determine the destination item for the new group item.
      const int pivotIndex = group->range().range_begin();
      TabStripItemIdentifier* destinationItemIdentifier =
          [self destinationItemAtIndex:pivotIndex parentGroup:nullptr];
      [self.consumer insertItems:@[ groupItemIdentifier ]
                      beforeItem:destinationItemIdentifier];
      // Ensure the group item is expanded if the group is not collapsed.
      // This is needed because items in a collection view are collapsed by
      // default.
      if (!group->visual_data().is_collapsed()) {
        [self.consumer expandGroup:groupItemIdentifier.tabGroupItem];
      }
      break;
    }
    case WebStateListChange::Type::kGroupVisualDataUpdate: {
      const WebStateListChangeGroupVisualDataUpdate& visualDataChange =
          change.As<WebStateListChangeGroupVisualDataUpdate>();
      const TabGroup* updatedGroup = visualDataChange.updated_group();
      TabGroupItem* updatedGroupItem =
          [[TabGroupItem alloc] initWithTabGroup:updatedGroup
                                    webStateList:webStateList];
      const bool oldCollapsed =
          visualDataChange.old_visual_data().is_collapsed();
      const bool newCollapsed = updatedGroup->visual_data().is_collapsed();
      if (oldCollapsed != newCollapsed) {
        if (newCollapsed) {
          const bool updateActiveIndex =
              updatedGroup->range().contains(webStateList->active_index());
          if (updateActiveIndex) {
            // If the active WebState will be collapsed, set the `selectItem`
            // to `nil` to ensure a smooth animation.
            [self.consumer selectItem:nil];
          }
          [self.consumer collapseGroup:updatedGroupItem];

          if (updateActiveIndex) {
            // If the active WebState is now collapsed, activate an existing or
            // new non-collapsed WebState.
            __weak __typeof(self) weakSelf = self;
            base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
                FROM_HERE, base::BindOnce(^{
                  [weakSelf activateExistingOrNewNonCollapsedWebState];
                }));
          }
        } else {
          [self.consumer expandGroup:updatedGroupItem];
        }
      }
      [self updateDataAndReconfigureItemsInGroup:updatedGroup];
      break;
    }
    case WebStateListChange::Type::kGroupMove: {
      const WebStateListChangeGroupMove& groupMoveChange =
          change.As<WebStateListChangeGroupMove>();
      TabStripItemIdentifier* itemIdentifier = CreateGroupItemIdentifier(
          groupMoveChange.moved_group(), _webStateList);
      const TabGroupRange toRange = groupMoveChange.moved_to_range();
      // Move item to new position.
      if (TabStripItemIdentifier* previousItemIdentifier =
              [self destinationItemAtIndex:(toRange.range_begin() - 1)
                               parentGroup:nil]) {
        // If after the update, there is a neighbor WebState to the left of the
        // group's range, then there are two cases where
        // `previousItemIdentifier` is not nil.
        // 1. The neighbor is not in a group (`previousItemIdentifier` is the
        // neighbor WebState).
        // 2. The neighbor is in a group (`previousItemIdentifier` is the
        // neighbor WebState's group).
        [self.consumer moveItem:itemIdentifier
                      afterItem:previousItemIdentifier];
      } else if (TabStripItemIdentifier* nextItemIdentifier =
                     [self destinationItemAtIndex:toRange.range_end()
                                      parentGroup:nil]) {
        // If after the update, there is a neighbor WebState to the right of the
        // group's range, then there are two cases where
        // `nextItemIdentifier` is not nil.
        // 1. The neighbor is not in a group (`nextItemIdentifier` is the
        // neighbor WebState).
        // 2. The neighbor is in a group (`nextItemIdentifier` is the neighbor
        // WebState's group).
        [self.consumer moveItem:itemIdentifier beforeItem:nextItemIdentifier];
      }
      break;
    }
    case WebStateListChange::Type::kGroupDelete: {
      const WebStateListChangeGroupDelete& groupDeleteChange =
          change.As<WebStateListChangeGroupDelete>();
      const TabGroup* group = groupDeleteChange.deleted_group();
      TabStripItemIdentifier* groupItemIdentifier =
          CreateGroupItemIdentifier(group, webStateList);
      [self.consumer removeItems:@[ groupItemIdentifier ]];
      break;
    }
  }

  // If there is a new active WebState, or the current active WebState moved or
  // changed status, ensure it is still selected and visible i.e. ensure that if
  // it is in a group, then this group is not collapsed.
  if (status.active_web_state_change() || activeWebStateDidMove ||
      activeWebStateDidChangeStatus) {
    const int activeIndex = webStateList->active_index();
    // If the selected index changes as a result of the last webstate being
    // detached, the active index will be -1.
    if (activeIndex == WebStateList::kInvalidIndex) {
      [self.consumer selectItem:nil];
      return;
    }
    TabSwitcherItem* item = [[WebStateTabSwitcherItem alloc]
        initWithWebState:status.new_active_web_state];
    [self.consumer selectItem:item];
    // If the active WebState is in a group, ensure that group is not collapsed.
    const TabGroup* groupOfActiveWebState =
        webStateList->GetGroupOfWebStateAt(activeIndex);
    if (groupOfActiveWebState &&
        groupOfActiveWebState->visual_data().is_collapsed()) {
      const tab_groups::TabGroupVisualData oldVisualData =
          groupOfActiveWebState->visual_data();
      const tab_groups::TabGroupVisualData newVisualData{
          oldVisualData.title(), oldVisualData.color(), /*is_collapsed=*/false};
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(&WebStateList::UpdateGroupVisualData,
                                    webStateList->AsWeakPtr(),
                                    groupOfActiveWebState, newVisualData));
    }
  }
}

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

  [self removeWebStateObservations];
}

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

  [self addWebStateObservations];
  [self populateConsumerItems];
}

#pragma mark - TabStripMutator

- (void)addNewItem {
  if (!self.webStateList || !self.browserState) {
    return;
  }
  const auto insertionParams = WebStateList::InsertionParams::Automatic();
  [self insertAndActivateNewWebStateWithInsertionParams:insertionParams];
}

- (void)activateItem:(TabSwitcherItem*)item {
  if (!self.webStateList) {
    return;
  }
  int index =
      GetWebStateIndex(self.webStateList, WebStateSearchCriteria{
                                              .identifier = item.identifier,
                                          });

  _webStateList->ActivateWebStateAt(index);
}

- (void)collapseGroup:(TabGroupItem*)tabGroupItem {
  if (!self.webStateList) {
    return;
  }
  base::RecordAction(base::UserMetricsAction("MobileTabStripGroupCollapse"));
  CHECK(tabGroupItem.tabGroup);
  const tab_groups::TabGroupVisualData oldVisualData =
      tabGroupItem.tabGroup->visual_data();
  const tab_groups::TabGroupVisualData newVisualData{
      oldVisualData.title(), oldVisualData.color(), /*is_collapsed=*/true};
  self.webStateList->UpdateGroupVisualData(tabGroupItem.tabGroup,
                                           newVisualData);
}

- (void)expandGroup:(TabGroupItem*)tabGroupItem {
  if (!self.webStateList) {
    return;
  }
  base::RecordAction(base::UserMetricsAction("MobileTabStripGroupExpand"));
  CHECK(tabGroupItem.tabGroup);
  const tab_groups::TabGroupVisualData oldVisualData =
      tabGroupItem.tabGroup->visual_data();
  const tab_groups::TabGroupVisualData newVisualData{
      oldVisualData.title(), oldVisualData.color(), /*is_collapsed=*/false};
  self.webStateList->UpdateGroupVisualData(tabGroupItem.tabGroup,
                                           newVisualData);
}

- (void)closeItem:(TabSwitcherItem*)item {
  if (!self.webStateList) {
    return;
  }

  int index = GetWebStateIndex(
      self.webStateList,
      WebStateSearchCriteria{
          .identifier = item.identifier,
          .pinned_state = WebStateSearchCriteria::PinnedState::kNonPinned,
      });
  if (index >= 0)
    self.webStateList->CloseWebStateAt(index, WebStateList::CLOSE_USER_ACTION);
}

- (void)removeItemFromGroup:(TabSwitcherItem*)item {
  if (!self.webStateList) {
    return;
  }
  int index =
      GetWebStateIndex(self.webStateList, WebStateSearchCriteria{
                                              .identifier = item.identifier,
                                          });
  self.webStateList->RemoveFromGroups({index});
}

- (void)closeAllItemsExcept:(TabSwitcherItem*)item {
  if (!self.webStateList) {
    return;
  }
  int indexToKeep = GetWebStateIndex(self.webStateList,
                                     WebStateSearchCriteria(item.identifier));

  int closedGroupCount = 0;
  if (IsTabGroupSyncEnabled()) {
    for (const TabGroup* group : _webStateList->GetGroups()) {
      // Remove the local tab group mapping if the `indexToKeep` is not in the
      // group.
      if (!group->range().contains(indexToKeep) &&
          _tabGroupSyncService->GetGroup(group->tab_group_id())) {
        _tabGroupSyncService->RemoveLocalTabGroupMapping(group->tab_group_id());
        closedGroupCount++;
      }
    }
  }

  // Closes all non-pinned items except for `item`.
  CloseOtherWebStates(*(self.webStateList), indexToKeep,
                      WebStateList::CLOSE_USER_ACTION);

  // Show the tab group snackbar if some groups have been closed.
  if (IsTabGroupSyncEnabled() && closedGroupCount > 0) {
    [self.tabStripHandler
        showTabStripTabGroupSnackbarAfterClosingGroups:closedGroupCount];
  }
}

- (void)createNewGroupWithItem:(TabSwitcherItem*)item {
  if (!self.webStateList) {
    return;
  }
  base::RecordAction(
      base::UserMetricsAction("MobileTabStripCreateGroupWithItem"));
  [_tabStripHandler showTabStripGroupCreationForTabs:{item.identifier}];
}

- (void)addItem:(TabSwitcherItem*)item
        toGroup:(const TabGroup*)destinationGroup {
  if (!self.webStateList || !self.browserState) {
    return;
  }
  base::RecordAction(base::UserMetricsAction("MobileTabStripAddItemToGroup"));

  const bool incognito = self.browserState->IsOffTheRecord();
  Browser* browserOfGroup =
      GetBrowserForGroup(_browserList, destinationGroup, incognito);

  if (self.browser == browserOfGroup) {
    int indexOfWebState =
        GetWebStateIndex(self.webStateList,
                         WebStateSearchCriteria{.identifier = item.identifier});
    self.webStateList->MoveToGroup({indexOfWebState}, destinationGroup);
    return;
  }

  MoveTabToBrowser(
      item.identifier, browserOfGroup,
      WebStateList::InsertionParams::Automatic().InGroup(destinationGroup));
}

- (void)renameGroup:(TabGroupItem*)tabGroupItem {
  if (!self.webStateList) {
    return;
  }
  base::RecordAction(base::UserMetricsAction("MobileTabStripRenameGroup"));
  [_tabStripHandler
      showTabStripGroupEditionForGroup:tabGroupItem.tabGroup->GetWeakPtr()];
}

- (void)addNewTabInGroup:(TabGroupItem*)tabGroupItem {
  if (!self.webStateList || !self.browserState) {
    return;
  }
  base::RecordAction(base::UserMetricsAction("MobileTabStripNewTabInGroup"));
  const auto insertionParams =
      WebStateList::InsertionParams::Automatic().InGroup(tabGroupItem.tabGroup);
  [self insertAndActivateNewWebStateWithInsertionParams:insertionParams];
}

- (void)ungroupGroup:(TabGroupItem*)tabGroupItem
          sourceView:(UIView*)sourceView {
  if (IsTabGroupSyncEnabled()) {
    // Show the confirmation dialog only when the tab group sync feature is
    // enabled.
    [_tabStripHandler
        showTabGroupConfirmationForAction:TabGroupActionType::kUngroupTabGroup
                                groupItem:tabGroupItem
                               sourceView:sourceView];
    return;
  }

  [self ungroupGroup:tabGroupItem];
}

- (void)deleteGroup:(TabGroupItem*)tabGroupItem sourceView:(UIView*)sourceView {
  if (IsTabGroupSyncEnabled()) {
    // Show the confirmation dialog only when the tab group sync feature is
    // enabled.
    [_tabStripHandler
        showTabGroupConfirmationForAction:TabGroupActionType::kDeleteTabGroup
                                groupItem:tabGroupItem
                               sourceView:sourceView];
    return;
  }

  [self deleteGroup:tabGroupItem];
}

- (void)closeGroup:(TabGroupItem*)tabGroupItem {
  if (!self.webStateList || !tabGroupItem.tabGroup) {
    return;
  }

  if (IsTabGroupSyncEnabled()) {
    tab_groups::utils::CloseTabGroupLocally(
        tabGroupItem.tabGroup, self.webStateList, _tabGroupSyncService);
    [self.tabStripHandler showTabStripTabGroupSnackbarAfterClosingGroups:1];
  } else {
    CloseAllWebStatesInGroup(*self.webStateList, tabGroupItem.tabGroup,
                             WebStateList::CLOSE_USER_ACTION);
  }
}

#pragma mark - CRWWebStateObserver

- (void)webStateDidStartLoading:(web::WebState*)webState {
  if (IsVisibleURLNewTabPage(webState)) {
    return;
  }
  [self reconfigureItemForWebState:webState];
}

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

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

#pragma mark - WebStateFaviconDriverObserver

- (void)faviconDriver:(favicon::FaviconDriver*)driver
    didUpdateFaviconForWebState:(web::WebState*)webState {
  [self reconfigureItemForWebState:webState];
}

#pragma mark - TabCollectionDragDropHandler

- (UIDragItem*)dragItemForItem:(TabSwitcherItem*)item {
  web::WebState* webState =
      GetWebState(_webStateList, WebStateSearchCriteria{
                                     .identifier = item.identifier,
                                 });
  return CreateTabDragItem(webState);
}

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

- (void)dragWillBeginForTabSwitcherItem:(TabSwitcherItem*)item {
  _dragItems = CreateItemIdentifiers(_webStateList,
                                     /*including_hidden_tab_items=*/false);
  // When a tab is dragged, it is visually removed from the collection view.
  [_dragItems removeObject:[TabStripItemIdentifier tabIdentifier:item]];
}

- (void)dragWillBeginForTabGroupItem:(TabGroupItem*)item {
  _dragItems = CreateItemIdentifiers(_webStateList,
                                     /*including_hidden_tab_items=*/false);
  // When a group is dragged, it is visually removed from the collection view,
  // along with all the tabs within that group.
  [_dragItems removeObject:[TabStripItemIdentifier groupIdentifier:item]];
  CHECK(item.tabGroup);
  for (int childWebStateIndex : item.tabGroup->range()) {
    TabStripItemIdentifier* childItemIdentifier = CreateTabItemIdentifier(
        _webStateList->GetWebStateAt(childWebStateIndex));
    [_dragItems removeObject:childItemIdentifier];
  }
}

- (void)dragSessionDidEnd {
  _dragItems = nil;
}

- (UIDropOperation)dropOperationForDropSession:(id<UIDropSession>)session
                                       toIndex:
                                           (NSUInteger)destinationItemIndex {
  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 (_browserState->IsOffTheRecord() == tabInfo.incognito) {
      return UIDropOperationMove;
    }

    // Tabs of different profiles (regular/incognito) cannot be dropped.
    return UIDropOperationForbidden;
  }

  // Group 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:[TabGroupInfo class]]) {
    TabGroupInfo* tabGroupInfo =
        base::apple::ObjCCast<TabGroupInfo>(dragItem.localObject);
    if (tabGroupInfo.browserState != self.browserState) {
      // Tabs from different profiles cannot be dropped.
      return UIDropOperationForbidden;
    }
    if (_dragItems && destinationItemIndex < _dragItems.count &&
        _dragItems[destinationItemIndex].tabSwitcherItem) {
      // If the drop originates from the same collection, then it is forbidden
      // to drop a group before an already grouped tab. If the drop originates
      // from a different collection view, a group can be dropped anywhere, but
      // it will be inserted at a valid location.
      int webStateIndex = GetWebStateIndex(
          _webStateList,
          WebStateSearchCriteria{
              .identifier =
                  _dragItems[destinationItemIndex].tabSwitcherItem.identifier});
      if (_webStateList->ContainsIndex(webStateIndex) &&
          _webStateList->GetGroupOfWebStateAt(webStateIndex)) {
        return UIDropOperationForbidden;
      }
    }

    if (self.browserState->IsOffTheRecord() == tabGroupInfo.incognito) {
      return UIDropOperationMove;
    }
    // Tabs of different profiles (regular/incognito) cannot be dropped.
    return 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 {
  // 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 (IsTabGroupSyncEnabled()) {
      BrowserAndIndex browserAndIndex = FindBrowserAndIndex(
          tabInfo.tabID,
          _browserList->BrowsersOfType(BrowserList::BrowserType::kRegular));
      if (browserAndIndex.browser) {
        const TabGroup* group =
            browserAndIndex.browser->GetWebStateList()->GetGroupOfWebStateAt(
                browserAndIndex.tab_index);
        if (group && group->range().count() == 1) {
          // `_tabGroupSyncService` is nullptr in incognito.
          const tab_groups::TabGroupId& localID = group->tab_group_id();
          if (_tabGroupSyncService && _tabGroupSyncService->GetGroup(localID)) {
            const base::Uuid savedID =
                _tabGroupSyncService->GetGroup(localID)->saved_guid();

            _tabGroupSyncService->RemoveLocalTabGroupMapping(localID);

            // Trying to move the last tab of group.
            TabStripLastTabDraggedAlertCommand* command =
                [[TabStripLastTabDraggedAlertCommand alloc] init];
            command.tabID = tabInfo.tabID;
            command.originBrowser = browserAndIndex.browser;
            command.originIndex = browserAndIndex.tab_index;
            command.visualData = group->visual_data();
            command.localGroupID = localID;
            command.savedGroupID = savedID;
            [_tabStripHandler showAlertForLastTabDragged:command];
          }
        }
      }
    }
    if (fromSameCollection) {
      base::UmaHistogramEnumeration(kUmaTabStripViewDragOrigin,
                                    DragItemOrigin::kSameCollection);
      // Reorder tabs.
      const WebStateList::InsertionParams insertionParams =
          [self insertionParamsForDestinationItemIndex:destinationIndex
                                                 items:_dragItems];
      MoveWebStateWithIdentifierToInsertionParams(
          tabInfo.tabID, insertionParams, _webStateList, fromSameCollection);
    } else {
      // The tab lives in another Browser.
      // TODO(crbug.com/41488813): Need to be updated for pinned tabs.
      base::UmaHistogramEnumeration(kUmaTabStripViewDragOrigin,
                                    DragItemOrigin::kOtherBrowser);
      [self moveItemWithIDFromDifferentBrowser:tabInfo.tabID
                                       toIndex:destinationIndex];
    }
    return;
  }

  // Group 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:[TabGroupInfo class]]) {
    TabGroupInfo* tabGroupInfo =
        base::apple::ObjCCast<TabGroupInfo>(dragItem.localObject);
    // Early return if the group has been closed during the drag an drop.
    if (!tabGroupInfo.tabGroup) {
      return;
    }
    if (fromSameCollection) {
      base::UmaHistogramEnumeration(kUmaTabStripViewGroupDragOrigin,
                                    DragItemOrigin::kSameCollection);
    } else {
      base::UmaHistogramEnumeration(kUmaTabStripViewGroupDragOrigin,
                                    DragItemOrigin::kOtherBrowser);
    }
    // Determine the tab strip item before which the group should be moved.
    NSArray<TabStripItemIdentifier*>* items = _dragItems;
    if (!items) {
      items = CreateItemIdentifiers(self.webStateList,
                                    /*including_hidden_tab_items=*/false);
    }
    TabStripItemIdentifier* nextItemIdentifier = nil;
    if (destinationIndex < items.count) {
      nextItemIdentifier = items[destinationIndex];
    }
    // Move the group before `nextItemIdentifier`.
    MoveGroupBeforeTabStripItem(tabGroupInfo.tabGroup, nextItemIdentifier,
                                self.browser);
    return;
  }
  base::UmaHistogramEnumeration(kUmaTabStripViewDragOrigin,
                                DragItemOrigin::kOther);

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

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

  __weak __typeof(self) 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 insertNewItemAtIndex:destinationIndex
                                 withURL:net::GURLWithNSURL(droppedURL)];
        });
      };
  [itemProvider loadObjectOfClass:[NSURL class] completionHandler:loadHandler];
}

#pragma mark - Private

// Adds an observation to every WebState of the current WebSateList.
- (void)addWebStateObservations {
  _allWebStateObservationForwarder =
      std::make_unique<AllWebStateObservationForwarder>(
          _webStateList, _webStateObserver.get());
}

// Removes an observation from every WebState of the current WebSateList.
- (void)removeWebStateObservations {
  _allWebStateObservationForwarder.reset();
}

// Updates the consumer with the list of all items and the selected one.
- (void)populateConsumerItems {
  TabSwitcherItem* selectedItem = nil;
  if (_webStateList->GetActiveWebState()) {
    selectedItem = [[WebStateTabSwitcherItem alloc]
        initWithWebState:_webStateList->GetActiveWebState()];
  }
  NSArray<TabStripItemIdentifier*>* itemIdentifiers =
      CreateItemIdentifiers(_webStateList);
  // Prepare tab strip item data (group stroke color, etc).
  NSDictionary<TabStripItemIdentifier*, TabStripItemData*>* itemData =
      [NSDictionary dictionaryWithObjects:CreateItemData(_webStateList)
                                  forKeys:itemIdentifiers];
  // Prepare item parents.
  NSMutableDictionary<TabStripItemIdentifier*, TabGroupItem*>* itemParents =
      [NSMutableDictionary dictionary];
  for (int index = 0; index < _webStateList->count(); ++index) {
    if (const TabGroup* parentGroup =
            _webStateList->GetGroupOfWebStateAt(index)) {
      TabGroupItem* parentTabGroupItem =
          [[TabGroupItem alloc] initWithTabGroup:parentGroup
                                    webStateList:_webStateList];
      TabStripItemIdentifier* itemIdentifier =
          CreateTabItemIdentifier(_webStateList->GetWebStateAt(index));
      [itemParents setObject:parentTabGroupItem forKey:itemIdentifier];
    }
  }
  [self.consumer populateWithItems:itemIdentifiers
                      selectedItem:selectedItem
                          itemData:itemData
                       itemParents:itemParents];
}

// Moves item to the desired final item index `itemIndexAfterUpdate`.
- (void)moveItemWithIDFromDifferentBrowser:(web::WebStateID)sourceWebStateID
                                   toIndex:(NSUInteger)itemIndexAfterUpdate {
  NSMutableArray<TabStripItemIdentifier*>* items = CreateItemIdentifiers(
      _webStateList, /*including_hidden_tab_items=*/false);

  if (itemIndexAfterUpdate >= items.count) {
    MoveTabToBrowser(sourceWebStateID, self.browser, _webStateList->count());
    return;
  }

  const WebStateList::InsertionParams insertionParams =
      [self insertionParamsForDestinationItemIndex:itemIndexAfterUpdate
                                             items:items];
  MoveTabToBrowser(sourceWebStateID, self.browser, insertionParams);
}

// Inserts a new item with the given`newTabURL` at `index`.
- (void)insertNewItemAtIndex:(NSUInteger)index withURL:(const GURL&)newTabURL {
  // 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 (_webStateList->IsMutating()) {
    // Shouldn't have happened!
    DCHECK(false) << "Reentrant web state insertion!";
    return;
  }
  DCHECK(_browserState);

  web::WebState::CreateParams params(_browserState);
  std::unique_ptr<web::WebState> webState = web::WebState::Create(params);

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

  // Simulating the insertion.
  NSMutableArray<TabStripItemIdentifier*>* items = CreateItemIdentifiers(
      _webStateList, /*including_hidden_tab_items=*/false);
  WebStateList::InsertionParams insertionParams =
      [self insertionParamsForDestinationItemIndex:index items:items];
  insertionParams.activate = true;
  _webStateList->InsertWebState(std::move(webState), insertionParams);
}

// Returns `InsertionParams` which can be used to move/insert a WebState so as
// to reflect the move/insertion of a new item at `destinationItemIndex` in
// `items`.
- (WebStateList::InsertionParams)
    insertionParamsForDestinationItemIndex:(NSUInteger)destinationItemIndex
                                     items:(NSArray<TabStripItemIdentifier*>*)
                                               items {
  TabStripItemIdentifier* previousItem =
      destinationItemIndex > 0 ? items[destinationItemIndex - 1] : nil;
  TabStripItemIdentifier* nextItem =
      destinationItemIndex < items.count ? items[destinationItemIndex] : nil;
  const TabGroup* destinationGroup =
      [self groupForInsertionBetweenPreviousItem:previousItem
                                        nextItem:nextItem];
  int webStateListInsertionIndex = 0;
  for (NSUInteger itemIndex = 0; itemIndex < destinationItemIndex;
       itemIndex++) {
    if (items[itemIndex].itemType == TabStripItemTypeTab) {
      webStateListInsertionIndex++;
      continue;
    }
    const TabGroup* group = items[itemIndex].tabGroupItem.tabGroup;
    if (group->visual_data().is_collapsed()) {
      webStateListInsertionIndex += group->range().count();
    }
  }
  return WebStateList::InsertionParams::AtIndex(webStateListInsertionIndex)
      .InGroup(destinationGroup);
}

// Returns the appropriate destination group for an item inserted between
// `previousItem` and `nextItem`.
- (const TabGroup*)
    groupForInsertionBetweenPreviousItem:(TabStripItemIdentifier*)previousItem
                                nextItem:(TabStripItemIdentifier*)nextItem {
  if (!nextItem.tabSwitcherItem) {
    // If the next item is not a tab, then the inserted item should have no
    // group.
    return nullptr;
  }

  const int indexOfNextWebState = GetWebStateIndex(
      _webStateList, WebStateSearchCriteria{
                         .identifier = nextItem.tabSwitcherItem.identifier,
                     });
  const TabGroup* groupOfNextWebState =
      _webStateList->GetGroupOfWebStateAt(indexOfNextWebState);
  if (!groupOfNextWebState) {
    // If the next item is not in a group, then the inserted item should have no
    // group either.
    return nullptr;
  }

  if (previousItem.tabGroupItem) {
    if (previousItem.tabGroupItem.tabGroup == groupOfNextWebState) {
      // If the previous item is the parent group item of the next item, then
      // the inserted item should be in that group.
      return groupOfNextWebState;
    }
    return nullptr;
  }

  // If the previous item is not a group item then it is a tab.
  int indexOfPreviousWebState = GetWebStateIndex(
      _webStateList, WebStateSearchCriteria{
                         .identifier = previousItem.tabSwitcherItem.identifier,
                     });
  if (!_webStateList->ContainsIndex(indexOfPreviousWebState)) {
    return nullptr;
  }
  const TabGroup* groupOfPreviousWebState =
      _webStateList->GetGroupOfWebStateAt(indexOfPreviousWebState);
  if (groupOfPreviousWebState == groupOfNextWebState) {
    // If the item is inserted between two tabs belonging to the same group,
    // then it should be inserted in that group.
    return groupOfNextWebState;
  }

  return nullptr;
}

// For each WebState in `group`, the associated item is reconfigured with an
// up-to-date `TabStripItemData`.
- (void)updateDataAndReconfigureItemsInGroup:(const TabGroup*)group {
  const TabGroupRange range = group->range();
  NSArray<TabStripItemIdentifier*>* tabItemIdentifiers =
      CreateItemIdentifiers(_webStateList, /*including_hidden_tab_items=*/false,
                            /*including_group_items=*/true, range);
  NSArray<TabStripItemData*>* tabItemData =
      CreateItemData(_webStateList, /*including_hidden_tab_items=*/false,
                     /*including_group_items=*/true, range);
  NSDictionary<TabStripItemIdentifier*, TabStripItemData*>* tabItemDataDict =
      [NSDictionary dictionaryWithObjects:tabItemData
                                  forKeys:tabItemIdentifiers];
  [self.consumer updateItemData:tabItemDataDict reconfigureItems:YES];
}

// Reconfigures the item associated with `webState`.
- (void)reconfigureItemForWebState:(web::WebState*)webState {
  const int webStateIndex = _webStateList->GetIndexOfWebState(webState);
  if (!_webStateList->ContainsIndex(webStateIndex)) {
    return;
  }
  const TabGroup* group = _webStateList->GetGroupOfWebStateAt(webStateIndex);
  if (group && group->visual_data().is_collapsed()) {
    // If group is collapsed then tab cells cannot be reconfigured.
    return;
  }
  [self.consumer reconfigureItems:@[ CreateTabItemIdentifier(webState) ]];
}

// Returns a destination item for insertion before/after `index`, such that the
// inserted item becomes a child of `parentGroup`. Returns nil if there is no
// such destination item e.g. if the group of the WebState at `index` and
// `parentGroup` are distinct groups.
- (TabStripItemIdentifier*)destinationItemAtIndex:(int)index
                                      parentGroup:(const TabGroup*)parentGroup {
  if (!_webStateList->ContainsIndex(index)) {
    return nil;
  }
  web::WebState* destinationWebState = _webStateList->GetWebStateAt(index);

  // Option 1. There is a `parentGroup`.

  if (parentGroup) {
    if (parentGroup->range().contains(index)) {
      // If the item at `index` also belongs to `parentGroup`, then it can be
      // used as a destination item.
      return CreateTabItemIdentifier(destinationWebState);
    }
    // Otherwise `item` cannot be used as a destination item to insert in
    // `parentGroup`.
    return nil;
  }

  // Option 2. There is no `parentGroup`.

  const TabGroup* groupOfDestinationWebState =
      _webStateList->GetGroupOfWebStateAt(index);
  if (groupOfDestinationWebState) {
    // If `item` does belongs to a group, then that group can be used as
    // destination item.
    return CreateGroupItemIdentifier(groupOfDestinationWebState, _webStateList);
  }

  // Otherwise if `item` also does not belong to a group, then it can be used as
  // a destination item.
  return CreateTabItemIdentifier(destinationWebState);
}

// Updates the tab strip items to reflect the move of `webState` in the
// WebStateList. This consists in moving the associated item to its appropriate
// new group (if any) and location, as well as updating the data associated with
// items in `oldGroup` and `newGroup`.
- (void)moveItemForWebState:(web::WebState*)webState
      fromWebStateListIndex:(int)fromIndex
        toWebStateListIndex:(int)toIndex
                   oldGroup:(const TabGroup*)oldGroup
                   newGroup:(const TabGroup*)newGroup {
  TabStripItemIdentifier* itemIdentifier = CreateTabItemIdentifier(webState);

  // Update item data.
  bool itemIsVisible = !newGroup || !newGroup->visual_data().is_collapsed();
  if (itemIsVisible) {
    TabStripItemData* itemData = CreateTabItemData(toIndex, _webStateList);
    [self.consumer updateItemData:@{itemIdentifier : itemData}
                 reconfigureItems:YES];
  }

  // Move item to new position.
  if (fromIndex != toIndex || oldGroup != newGroup) {
    if (TabStripItemIdentifier* previousItemIdentifier =
            [self destinationItemAtIndex:(toIndex - 1) parentGroup:newGroup]) {
      [self.consumer moveItem:itemIdentifier afterItem:previousItemIdentifier];
    } else if (TabStripItemIdentifier* nextItemIdentifier =
                   [self destinationItemAtIndex:(toIndex + 1)
                                    parentGroup:newGroup]) {
      [self.consumer moveItem:itemIdentifier beforeItem:nextItemIdentifier];
    } else if (newGroup) {
      TabGroupItem* newGroupItem =
          [[TabGroupItem alloc] initWithTabGroup:newGroup
                                    webStateList:_webStateList];
      [self.consumer moveItem:itemIdentifier insideGroup:newGroupItem];
    } else {
      [self.consumer moveItem:itemIdentifier beforeItem:nil];
    }
  }

  // Reconfigure the old and new group items if needed.
  if (oldGroup) {
    [self updateDataAndReconfigureItemsInGroup:oldGroup];
  }
  if (newGroup) {
    [self updateDataAndReconfigureItemsInGroup:newGroup];
  }
}

// Inserts and activate a new WebState opened at `kChromeUINewTabURL` using
// `insertionParams`.
- (void)insertAndActivateNewWebStateWithInsertionParams:
    (WebStateList::InsertionParams)insertionParams {
  CHECK(self.browserState);
  CHECK(self.webStateList);

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

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

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

  self.webStateList->InsertWebState(std::move(webState),
                                    insertionParams.Activate());
}

// Returns whether the WebState at `index` is collapsed i.e. it is inside of a
// collapsed TabGroup.
- (BOOL)webStateIsCollapsedAtIndex:(int)index {
  if (!self.webStateList->ContainsIndex(index)) {
    return NO;
  }
  const TabGroup* groupAtIndex = self.webStateList->GetGroupOfWebStateAt(index);
  return groupAtIndex && groupAtIndex->visual_data().is_collapsed();
}

// Activates a non-collapsed WebState close to the current active WebState. If
// all WebStates in the WebStateList are collapsed, inserts a new WebState and
// activates it.
- (void)activateExistingOrNewNonCollapsedWebState {
  int activeIndex = self.webStateList->active_index();
  if (!self.webStateList->ContainsIndex(activeIndex)) {
    return;
  }
  // If the active WebState is not collapsed, there is nothing to do.
  if (![self webStateIsCollapsedAtIndex:activeIndex]) {
    return;
  }
  // If the active WebState is collapsed, find the closest WebState that is not
  // collapsed.
  const int indexOfNewActiveWebState =
      [self indexOfNonCollapsedWebStateCloseToCollapsedWebStateAt:activeIndex];
  if (self.webStateList->ContainsIndex(indexOfNewActiveWebState)) {
    // If there is a WebState that is not collapsed, activate that WebState.
    self.webStateList->ActivateWebStateAt(indexOfNewActiveWebState);
    return;
  }
  // If there is no WebState to activate on the right or on the left, insert
  // and activate a new WebState at the end of the WebStateList instead.
  const auto insertionParams = WebStateList::InsertionParams::Automatic();
  [self insertAndActivateNewWebStateWithInsertionParams:insertionParams];
}

// Returns the index of a non-collapsed WebState close to `index`. If all
// WebStates in the WebStateList are collapsed, returns `kInvalidIndex`.
- (int)indexOfNonCollapsedWebStateCloseToCollapsedWebStateAt:(int)index {
  CHECK([self webStateIsCollapsedAtIndex:index]);
  // If the tab for WebState at `index` is collapsed, then it must be in a group
  // that is collapsed.
  CHECK(self.webStateList->ContainsIndex(index), base::NotFatalUntil::M128);
  const TabGroup* groupAtIndex = self.webStateList->GetGroupOfWebStateAt(index);
  CHECK(groupAtIndex);
  CHECK(groupAtIndex->visual_data().is_collapsed());
  // If the WebState at `index` is collapsed, find the first WebState to the
  // right of `index` that is not collapsed.
  int newActiveIndex = groupAtIndex->range().range_end();
  while (self.webStateList->ContainsIndex(newActiveIndex)) {
    if (![self webStateIsCollapsedAtIndex:newActiveIndex]) {
      return newActiveIndex;
    }
    newActiveIndex++;
  }
  // If all WebStates to the right of `index` are collapsed, find the first
  // WebState to the left of `index` that is not collapsed.
  newActiveIndex = groupAtIndex->range().range_begin() - 1;
  while (self.webStateList->ContainsIndex(newActiveIndex)) {
    if (![self webStateIsCollapsedAtIndex:newActiveIndex]) {
      return newActiveIndex;
    }
    newActiveIndex--;
  }
  // If all WebStates in the WebStateList are collapsed, return
  // `WebStateList::kInvalidIndex`.
  return WebStateList::kInvalidIndex;
}

@end