chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_view_controller.mm

// Copyright 2018 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/tab_grid_view_controller.h"

#import <objc/runtime.h>

#import "base/debug/dump_without_crashing.h"
#import "base/functional/bind.h"
#import "base/ios/ios_util.h"
#import "base/logging.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/notimplemented.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/bubble/ui_bundled/gesture_iph/gesture_in_product_help_view.h"
#import "ios/chrome/browser/bubble/ui_bundled/gesture_iph/gesture_in_product_help_view_delegate.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/tab_grid_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/legacy_chrome_table_view_styler.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/browser/tabs/model/inactive_tabs/features.h"
#import "ios/chrome/browser/ui/menu/action_factory.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_table_view_controller.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_table_view_controller_ui_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_drag_drop_handler.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/disabled_grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_commands.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_container_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_empty_state_view.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/incognito/incognito_grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/regular/regular_grid_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/suggested_actions/suggested_actions_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_context_menu/tab_context_menu_provider.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_activity_observer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_constants.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_consumer.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_mutator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_groups_panel_view_controller.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_bottom_toolbar.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_new_tab_button.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_page_control.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_top_toolbar.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/legacy_grid_transition_layout.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/transitions/tab_grid_transition_layout.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "ios/web/public/web_state_id.h"
#import "ui/base/l10n/l10n_util.h"

namespace {

// Types of configurations of this view controller.
typedef NS_ENUM(NSUInteger, TabGridConfiguration) {
  TabGridConfigurationBottomToolbar = 1,
  TabGridConfigurationFloatingButton,
};

// Computes the page from the offset and width of `scrollView`.
TabGridPage GetPageFromScrollView(UIScrollView* scrollView) {
  CGFloat pageWidth = scrollView.frame.size.width;
  CGFloat offset = scrollView.contentOffset.x;
  NSUInteger page = lround(offset / pageWidth);
  // Fence `page` to valid values; page values of 3 (rounded up from 2.5) are
  // possible, as are large int values if `pageWidth` is somehow very small.
  page = page < TabGridPageIncognitoTabs ? TabGridPageIncognitoTabs : page;
  page = page > TabGridPageRemoteTabs ? TabGridPageRemoteTabs : page;
  TabGridPage tabGridPage = static_cast<TabGridPage>(page);
  if (UseRTLLayout()) {
    // In RTL, page indexes are inverted, so subtract `page` from the
    // TabGridPageRemoteTabs value.
    tabGridPage = static_cast<TabGridPage>(TabGridPageRemoteTabs - page);
  }
  // With Tab Group Sync, the last page is actually Tab Groups, not Remote Tabs.
  // So do the swap before returning the page.
  if (IsTabGroupSyncEnabled() && tabGridPage == TabGridPageRemoteTabs) {
    tabGridPage = TabGridPageTabGroups;
  }
  return tabGridPage;
}

NSUInteger GetPageIndexFromPage(TabGridPage page) {
  // With Tab Group Sync, the last page is actually Tab Groups, not Remote Tabs.
  // But this method computes the page index out of the enum value, so simulate
  // being Remote Tabs…
  if (IsTabGroupSyncEnabled() && page == TabGridPageTabGroups) {
    page = TabGridPageRemoteTabs;
  }
  if (UseRTLLayout()) {
    // In RTL, page indexes are inverted, so subtract `page` from the highest-
    // index TabGridPage value.
    return static_cast<NSUInteger>(TabGridPageRemoteTabs - page);
  }
  return static_cast<NSUInteger>(page);
}
}  // namespace

@interface TabGridViewController () <GestureInProductHelpViewDelegate,
                                     GridViewControllerDelegate,
                                     PinnedTabsViewControllerDelegate,
                                     RecentTabsTableViewControllerUIDelegate,
                                     TabGroupsPanelViewControllerUIDelegate,
                                     UIGestureRecognizerDelegate,
                                     UIScrollViewAccessibilityDelegate>
// Whether the view is visible. Bookkeeping is based on
// `-contentWillAppearAnimated:` and
// `-contentWillDisappearAnimated methods. Note that the `Did` methods are not
// reliably called (e.g., edge case in multitasking).
@property(nonatomic, assign) BOOL viewVisible;

// The view controller to display when the recent tabs are disabled.
@property(nonatomic, strong)
    DisabledGridViewController* remoteDisabledViewController;

// Redefined as readwrite
@property(nonatomic, assign, readwrite) TabGridPage activePage;
// Setting the current page doesn't scroll the scroll view; use
// -scrollToPage:animated: for that. Redefined as readwrite.
@property(nonatomic, assign, readwrite) TabGridPage currentPage;

// Other UI components.
@property(nonatomic, weak) UIScrollView* scrollView;
@property(nonatomic, weak) UIView* scrollContentView;
// Scrim view to be presented when the search box in focused with no text.
@property(nonatomic, strong) UIControl* scrimView;
@property(nonatomic, assign) TabGridConfiguration configuration;
// The UIViewController corresponding with `currentPage`.
@property(nonatomic, readonly) UIViewController* currentPageViewController;
// Whether the scroll view is animating its content offset to the current page.
@property(nonatomic, assign, getter=isScrollViewAnimatingContentOffset)
    BOOL scrollViewAnimatingContentOffset;
// Constraints for the pinned tabs view.
@property(nonatomic, strong)
    NSArray<NSLayoutConstraint*>* pinnedTabsConstraints;
// The configuration for tab grid pages.
@property(nonatomic, assign) TabGridPageConfiguration pageConfiguration;
// Wether there is a search being performed in the tab grid or not.
@property(nonatomic, assign) BOOL isPerformingSearch;
// Pan gesture for when the search results view is scrolled during the search
// mode.
@property(nonatomic, strong) UIPanGestureRecognizer* searchResultPanRecognizer;

@property(nonatomic, assign, getter=isDragSessionInProgress)
    BOOL dragSessionInProgress;

// The timestamp of the user entering the tab grid.
@property(nonatomic, assign) base::TimeTicks tabGridEnterTime;

// The in-product help view to instruct the user to swipe to incognito, and its
// bottom constraint.
@property(nonatomic, strong) GestureInProductHelpView* swipeToIncognitoIPH;
@property(nonatomic, strong)
    NSLayoutConstraint* swipeToIncognitoIPHBottomConstraint;

@end

@implementation TabGridViewController {
  // Searched text.
  NSString* _searchText;
  // Idle page status.
  // Tracks whether the user closed the tab switcher without doing any
  // `TabGridActionType::kInPageAction`s.
  BOOL _idleTabGrid;
  // Whether the user has done anything meaningful when the third page is
  // visible.
  BOOL _idleThirdPage;
  // Whether the user has changed pages since entering the tab grid.
  BOOL _pageChangedSinceEntering;
  // Whether the user has put the app to background since entering tab grid.
  BOOL _backgroundedSinceEntering;
  // Current mode of the TabGrid.
  TabGridMode _mode;
}

- (instancetype)initWithPageConfiguration:
    (TabGridPageConfiguration)tabGridPageConfiguration {
  self = [super initWithNibName:nil bundle:nil];
  if (self) {
    _pageConfiguration = tabGridPageConfiguration;
    _dragSessionInProgress = NO;

    if (!IsTabGroupSyncEnabled()) {
      // TODO(crbug.com/41390276): This should move to a proper Recent Tabs in
      // Grid coordinator.
      if (_pageConfiguration == TabGridPageConfiguration::kIncognitoPageOnly) {
        _remoteDisabledViewController = [[DisabledGridViewController alloc]
            initWithPage:TabGridPageRemoteTabs];
        _remoteDisabledViewController.delegate = self;
      } else {
        _remoteTabsViewController =
            [[RecentTabsTableViewController alloc] init];
      }
    }
  }
  return self;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  self.view.backgroundColor = [UIColor colorNamed:kGridBackgroundColor];
  [self setupScrollView];

  if (!IsTabGroupSyncEnabled()) {
    if (_pageConfiguration == TabGridPageConfiguration::kIncognitoPageOnly) {
      [self setupDisabledRemoteTabsViewController];
    } else {
      [self setupRemoteTabsViewController];
    }
  }

  [self setupSearchUI];
  [self setupTopToolbar];
  [self setupBottomToolbar];

  if (IsPinnedTabsEnabled()) {
    CHECK(self.pinnedTabsViewController);
    [self setupPinnedTabsViewController];
  }

  // Hide the toolbars and the floating button, so they can fade in the first
  // time there's a transition into this view controller.
  [self hideToolbars];
}

- (void)viewDidLayoutSubviews {
  [super viewDidLayoutSubviews];
  // Modify Incognito and Regular Tabs Insets.
  [self setInsetForGridViews];
}

- (void)viewWillTransitionToSize:(CGSize)size
       withTransitionCoordinator:
           (id<UIViewControllerTransitionCoordinator>)coordinator {
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  __weak TabGridViewController* weakSelf = self;
  auto animate = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
    [weakSelf animateTransition:context];
  };
  [coordinator animateAlongsideTransition:animate completion:nil];
}

- (void)animateTransition:
    (id<UIViewControllerTransitionCoordinatorContext>)context {
  // Sync the scroll view offset to the current page value. Since this is
  // invoked inside an animation block, the scrolling doesn't need to be
  // animated.
  [self scrollToPage:_currentPage animated:NO];
  [self configureViewControllerForCurrentSizeClassesAndPage];
  [self setInsetForRemoteTabs];
  [self setInsetForGridViews];
}

- (UIStatusBarStyle)preferredStatusBarStyle {
  return UIStatusBarStyleLightContent;
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  if (IsPinnedTabsEnabled()) {
    [self updatePinnedTabsViewControllerConstraints];
  }
  if ([self.swipeToIncognitoIPH superview] == self.view) {
    self.swipeToIncognitoIPHBottomConstraint.active = NO;
    self.swipeToIncognitoIPHBottomConstraint =
        [self.swipeToIncognitoIPH.bottomAnchor
            constraintEqualToAnchor:[self shouldUseCompactLayout]
                                        ? self.bottomToolbar.topAnchor
                                        : self.regularTabsViewController.view
                                              .bottomAnchor];

    self.swipeToIncognitoIPHBottomConstraint.active = YES;
  }
}

#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
  if (scrollView.dragging || scrollView.decelerating) {
    // Only when user initiates scroll through dragging.
    CGFloat offsetWidth =
        self.scrollView.contentSize.width - self.scrollView.frame.size.width;
    CGFloat offset = scrollView.contentOffset.x / offsetWidth;
    // In RTL, flip the offset.
    if (UseRTLLayout()) {
      offset = 1.0 - offset;
    }
    self.topToolbar.pageControl.sliderPosition = offset;

    TabGridPage page = GetPageFromScrollView(scrollView);
    if (page != self.currentPage) {
      // Records when the user drags the scrollView to switch pages.
      [self.mutator pageChanged:page
                    interaction:TabSwitcherPageChangeInteraction::kScrollDrag];
      self.currentPage = page;
      [self broadcastIncognitoContentVisibility];
    }
  }
}

- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
  // Disable the page control when the user drags on the scroll view since
  // tapping on the page control during scrolling can result in erratic
  // scrolling.
  self.topToolbar.pageControl.userInteractionEnabled = NO;
}

- (void)scrollViewDidEndDragging:(UIScrollView*)scrollView
                  willDecelerate:(BOOL)decelerate {
  // Re-enable the page control since the user isn't dragging anymore.
  self.topToolbar.pageControl.userInteractionEnabled = YES;
}

- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView {
  // Update currentPage if scroll view has moved to a new page. Especially
  // important here for 3-finger accessibility swipes since it's not registered
  // as dragging in scrollViewDidScroll:
  TabGridPage page = GetPageFromScrollView(scrollView);
  if (page != self.currentPage) {
    [self.mutator
        pageChanged:page
        interaction:TabSwitcherPageChangeInteraction::kAccessibilitySwipe];
    self.currentPage = page;
    [self broadcastIncognitoContentVisibility];
    [self.topToolbar.pageControl setSelectedPage:page animated:YES];
  }
}

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView*)scrollView {
  TabGridPage currentPage = GetPageFromScrollView(scrollView);
  if (currentPage != self.currentPage && self.isDragSessionInProgress) {
    // This happens when the user drags an item from one scroll view into
    // another.
    [self.mutator pageChanged:currentPage
                  interaction:TabSwitcherPageChangeInteraction::kItemDrag];
    [self.topToolbar.pageControl setSelectedPage:currentPage animated:YES];
  }
  self.currentPage = currentPage;
  self.scrollViewAnimatingContentOffset = NO;
  [self broadcastIncognitoContentVisibility];
  if (!self.isDragSessionInProgress) {
    [self maybeShowSwipeToIncognitoIPH];
  }
}

#pragma mark - Accessibility

- (BOOL)accessibilityPerformEscape {
  [self.tabGridHandler exitTabGrid];
  return YES;
}

#pragma mark - UIScrollViewAccessibilityDelegate

- (NSString*)accessibilityScrollStatusForScrollView:(UIScrollView*)scrollView {
  // This reads the new page whenever the user scrolls in VoiceOver.
  int stringID;
  switch (self.currentPage) {
    case TabGridPageIncognitoTabs:
      stringID = IDS_IOS_TAB_GRID_INCOGNITO_TABS_TITLE;
      break;
    case TabGridPageRegularTabs:
      if (IsTabGroupInGridEnabled()) {
        stringID = IDS_IOS_TAB_GRID_REGULAR_TABS_WITH_GROUPS_TITLE;
      } else {
        stringID = IDS_IOS_TAB_GRID_REGULAR_TABS_TITLE;
      }
      break;
    case TabGridPageRemoteTabs:
      stringID = IDS_IOS_TAB_GRID_REMOTE_TABS_TITLE;
      break;
    case TabGridPageTabGroups:
      stringID = IDS_IOS_TAB_GRID_TAB_GROUPS_TITLE;
      break;
  }
  return l10n_util::GetNSString(stringID);
}

#pragma mark - TabGridTransitionLayoutProviding

- (TabGridTransitionLayout*)transitionLayout {
  TabGridTransitionItem* activeCell =
      [self transitionItemForActiveCellWithActivePage:self.activePage];
  return [TabGridTransitionLayout layoutWithActiveCell:activeCell];
}

#pragma mark - Public Methods

- (void)contentWillAppearAnimated:(BOOL)animated {
  _pageChangedSinceEntering = NO;
  _backgroundedSinceEntering = NO;
  [self resetIdlePageStatus];
  self.viewVisible = YES;
  [self.topToolbar.pageControl setSelectedPage:self.currentPage animated:NO];
  [self configureViewControllerForCurrentSizeClassesAndPage];

  // The toolbars should be hidden (alpha 0.0) before the tab appears, so that
  // they can be animated in. They can't be set to 0.0 here, because if
  // `animated` is YES, this method is being called inside the animation block.
  if (animated && self.transitionCoordinator) {
    [self animateToolbarsForAppearance];
  } else {
    [self showToolbars];
  }
  [self broadcastIncognitoContentVisibility];

  [self.incognitoTabsViewController contentWillAppearAnimated:animated];
  [self.regularTabsViewController contentWillAppearAnimated:animated];
  [self.pinnedTabsViewController contentWillAppearAnimated:animated];

  self.remoteTabsViewController.session = self.view.window.windowScene.session;

  self.remoteTabsViewController.preventUpdates = NO;

  // Record when the tab switcher is presented.
  self.tabGridEnterTime = base::TimeTicks::Now();
}

- (void)contentDidAppear {
  // Modify Remote Tabs Insets when page appears and during rotation.
  if (self.remoteTabsViewController) {
    [self setInsetForRemoteTabs];
  }
  [self maybeShowSwipeToIncognitoIPH];
}

- (void)contentWillDisappearAnimated:(BOOL)animated {
  [self recordIdlePageStatus];

  [self.regularGridHandler discardSavedClosedItems];

  [self.swipeToIncognitoIPH
      dismissWithReason:IPHDismissalReasonType::kTappedOutsideIPHAndAnchorView];

  // When the view disappears, the toolbar alpha should be set to 0; either as
  // part of the animation, or directly with -hideToolbars.
  if (animated && self.transitionCoordinator) {
    [self animateToolbarsForDisappearance];
  } else {
    [self hideToolbars];
  }

  self.viewVisible = NO;

  [self.pinnedTabsViewController contentWillDisappear];
  self.remoteTabsViewController.preventUpdates = YES;

  self.tabGridEnterTime = base::TimeTicks();
}

- (void)dismissModals {
  [self.remoteTabsViewController dismissModals];
}

- (void)setCurrentPageAndPageControl:(TabGridPage)page animated:(BOOL)animated {
  [self updatePageWithCurrentSearchTerms:page];

  if (self.topToolbar.pageControl.selectedPage != page)
    [self.topToolbar.pageControl setSelectedPage:page animated:animated];
  if (self.currentPage != page) {
    [self.mutator pageChanged:page
                  interaction:TabSwitcherPageChangeInteraction::kNone];
    self.currentPage = page;
    [self scrollToPage:page animated:animated];
  }
}

// Sets the current search terms on `page`. This allows the content to update
// while the page is still hidden before the page change animation begins.
- (void)updatePageWithCurrentSearchTerms:(TabGridPage)page {
  if (_mode != TabGridMode::kSearch ||
      self.currentPage == TabGridPageIncognitoTabs) {
    // No need to update search term if not in search mode or currently on the
    // incognito page.
    return;
  }

  NSString* searchTerms = nil;
  if (self.currentPage == TabGridPageRegularTabs) {
    searchTerms = self.regularTabsViewController.searchText;
  } else {
    searchTerms = self.remoteTabsViewController.searchTerms;
  }

  if (page == TabGridPageRegularTabs) {
    // Search terms will be non-empty when switching pages. This is important
    // because `searchItemsWithText:` will show items from all windows. When no
    // search terms exist, `resetToAllItems` is used instead.
    DCHECK(searchTerms.length);
    self.regularTabsViewController.searchText = searchTerms;
    [self.regularGridHandler searchItemsWithText:searchTerms];
  } else {
    self.remoteTabsViewController.searchTerms = searchTerms;
  }
}

- (void)updateActivePageToCurrent {
  TabGridPage newActivePage = self.currentPage;

  if (self.currentPage == TabGridPageRemoteTabs ||
      self.currentPage == TabGridPageTabGroups) {
    _idleThirdPage = YES;
    newActivePage = self.activePage;
  }

  [self.mutator pageChanged:newActivePage
                interaction:TabSwitcherPageChangeInteraction::kNone];
  self.activePage = newActivePage;
}

#pragma mark - Public Properties

- (void)setIncognitoTabsViewController:
    (IncognitoGridViewController*)incognitoTabsViewController {
  _incognitoTabsViewController = incognitoTabsViewController;
  _incognitoTabsViewController.delegate = self;
  _incognitoTabsViewController.view.accessibilityElementsHidden =
      self.currentPage != TabGridPageIncognitoTabs;
}

- (void)setIncognitoDisabledGridViewController:
    (UIViewController*)incognitoDisabledGridViewController {
  _incognitoDisabledGridViewController = incognitoDisabledGridViewController;
  _incognitoDisabledGridViewController.view.accessibilityElementsHidden =
      self.currentPage != TabGridPageIncognitoTabs;
}

- (void)setRegularTabsViewController:
    (RegularGridViewController*)regularTabsViewController {
  _regularTabsViewController = regularTabsViewController;
  _regularTabsViewController.delegate = self;
  _regularTabsViewController.view.accessibilityElementsHidden =
      self.currentPage != TabGridPageRegularTabs;
}

- (void)setRegularDisabledGridViewController:
    (UIViewController*)regularDisabledGridViewController {
  _regularDisabledGridViewController = regularDisabledGridViewController;
  _regularDisabledGridViewController.view.accessibilityElementsHidden =
      self.currentPage != TabGridPageRegularTabs;
}

- (void)setTabGroupsPanelViewController:
    (TabGroupsPanelViewController*)tabGroupsPanelViewController {
  _tabGroupsPanelViewController = tabGroupsPanelViewController;
  _tabGroupsPanelViewController.UIDelegate = self;
  _tabGroupsPanelViewController.view.accessibilityElementsHidden =
      self.currentPage != TabGridPageTabGroups;
}

- (void)setTabGroupsDisabledGridViewController:
    (UIViewController*)tabGroupsDisabledGridViewController {
  _tabGroupsDisabledGridViewController = tabGroupsDisabledGridViewController;
  _tabGroupsDisabledGridViewController.view.accessibilityElementsHidden =
      self.currentPage != TabGridPageTabGroups;
}

- (void)setRemoteDisabledViewController:
    (DisabledGridViewController*)remoteDisabledViewController {
  _remoteDisabledViewController = remoteDisabledViewController;
  _remoteDisabledViewController.view.accessibilityElementsHidden =
      self.currentPage != TabGridPageRemoteTabs;
}

- (void)setPriceCardDataSource:(id<PriceCardDataSource>)priceCardDataSource {
  self.regularTabsViewController.priceCardDataSource = priceCardDataSource;
  _priceCardDataSource = priceCardDataSource;
}

- (id<RecentTabsConsumer>)remoteTabsConsumer {
  return self.remoteTabsViewController;
}

#pragma mark - Private

// Records the idle page status for the current `currentPage`.
- (void)recordIdlePageStatus {
  if (!self.viewVisible) {
    return;
  }

  switch (self.currentPage) {
    case TabGridPage::TabGridPageIncognitoTabs:
      base::UmaHistogramBoolean(
          kUMATabSwitcherIdleIncognitoTabGridPageHistogram, _idleTabGrid);
      break;
    case TabGridPage::TabGridPageRegularTabs:
      base::UmaHistogramBoolean(kUMATabSwitcherIdleRegularTabGridPageHistogram,
                                _idleTabGrid);
      break;
    case TabGridPage::TabGridPageRemoteTabs:
      base::UmaHistogramBoolean(kUMATabSwitcherIdleRecentTabsHistogram,
                                _idleThirdPage);
      break;
    case TabGridPage::TabGridPageTabGroups:
      base::UmaHistogramBoolean(kUMATabSwitcherIdleTabGroupsHistogram,
                                _idleThirdPage);
      break;
  }
}

// Resets idle page status.
- (void)resetIdlePageStatus {
  _idleTabGrid = YES;
  // `_idleThirdPage` is set to 'YES' if the "Done" button has been tapped from
  // the third page or if the page has changed.
  _idleThirdPage = NO;
}

// Sets the proper insets for the Remote Tabs ViewController to accommodate for
// the safe area, toolbar, and status bar.
- (void)setInsetForRemoteTabs {
  // Sync the scroll view offset to the current page value if the scroll view
  // isn't scrolling. Don't animate this.
  if (!self.scrollView.dragging && !self.scrollView.decelerating) {
    [self scrollToPage:self.currentPage animated:NO];
  }
  // The content inset of the tab grids must be modified so that the toolbars
  // do not obscure the tabs. This may change depending on orientation.
  CGFloat bottomInset = self.bottomToolbar.intrinsicContentSize.height;
  UIEdgeInsets inset = UIEdgeInsetsMake(
      self.topToolbar.intrinsicContentSize.height, 0, bottomInset, 0);
  // Left and right side could be missing correct safe area
  // inset upon rotation. Manually correct it.
  self.remoteTabsViewController.additionalSafeAreaInsets = UIEdgeInsetsZero;
  UIEdgeInsets additionalSafeArea = inset;
  UIEdgeInsets safeArea = self.scrollView.safeAreaInsets;
  // If Remote Tabs isn't on the screen, it will not have the right safe area
  // insets. Pass down the safe area insets of the scroll view.
  if (self.currentPage != TabGridPageRemoteTabs) {
    additionalSafeArea.right = safeArea.right;
    additionalSafeArea.left = safeArea.left;
  }

  // Ensure that the View Controller doesn't have safe area inset that already
  // covers the view's bounds. This can happen in tests.
  if (!CGRectIsEmpty(UIEdgeInsetsInsetRect(
          self.remoteTabsViewController.tableView.bounds,
          self.remoteTabsViewController.tableView.safeAreaInsets))) {
    self.remoteTabsViewController.additionalSafeAreaInsets = additionalSafeArea;
  }
}

// Sets the proper insets for the Grid ViewControllers to accommodate for the
// safe area and toolbars.
- (void)setInsetForGridViews {
  // Sync the scroll view offset to the current page value if the scroll view
  // isn't scrolling. Don't animate this.
  if (!self.scrollViewAnimatingContentOffset && !self.scrollView.dragging &&
      !self.scrollView.decelerating) {
    [self scrollToPage:self.currentPage animated:NO];
  }

  self.incognitoTabsViewController.contentInsets =
      [self calculateInsetsForGridView];
  self.regularTabsViewController.contentInsets =
      [self calculateInsetsForRegularGridView];
  self.tabGroupsPanelViewController.contentInsets =
      [self calculateInsetsForGridView];
}

// Returns the corresponding BaseGridViewController for `page`. Returns `nil` if
// page does not have a corresponding BaseGridViewController.
- (BaseGridViewController*)gridViewControllerForPage:(TabGridPage)page {
  switch (page) {
    case TabGridPageIncognitoTabs:
      return self.incognitoTabsViewController;
    case TabGridPageRegularTabs:
      return self.regularTabsViewController;
    case TabGridPageRemoteTabs:
    case TabGridPageTabGroups:
      return nil;
  }
}

- (void)setActivePage:(TabGridPage)activePage {
  [self scrollToPage:activePage animated:YES];
  [self.activityObserver updateLastActiveTabPage:activePage];
  if (activePage != _activePage) {
    // Usually, an active page change is a result of an in-page action happening
    // on a previously non-active page.
    [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
  }
  _activePage = activePage;
}

- (void)setCurrentPage:(TabGridPage)currentPage {
  // Record the idle metric if the previous page was the third panel.
  if (_currentPage != currentPage) {
    [self tabGridDidPerformAction:TabGridActionType::kChangePage];
    if (_currentPage == TabGridPageRemoteTabs ||
        _currentPage == TabGridPageTabGroups) {
      _idleThirdPage = YES;
      [self recordIdlePageStatus];
      _idleThirdPage = NO;
    }
  }

  // Original current page is about to not be visible. Disable it from being
  // focused by VoiceOver.
  self.currentPageViewController.view.accessibilityElementsHidden = YES;
  UIViewController* previousPageVC = self.currentPageViewController;
  _currentPage = currentPage;
  self.currentPageViewController.view.accessibilityElementsHidden = NO;

  if (_mode == TabGridMode::kSearch) {
    // `UIAccessibilityLayoutChangedNotification` doesn't change the current
    // item focused by the voiceOver if the notification argument provided with
    // it is `nil`. In search mode, the item focused by the voiceOver needs to
    // be reset and to do that `UIAccessibilityScreenChangedNotification` should
    // be posted instead.
    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
                                    nil);
    // If the search mode is active. the previous page should have the result
    // gesture recognizer installed, make sure to move the gesture recognizer to
    // the new page's view.
    [previousPageVC.view
        removeGestureRecognizer:self.searchResultPanRecognizer];
    [self.currentPageViewController.view
        addGestureRecognizer:self.searchResultPanRecognizer];
  } else {
    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
                                    nil);
  }
  // Dismiss IPH if not on regular page.
  if (currentPage != TabGridPage::TabGridPageRegularTabs) {
    [self.swipeToIncognitoIPH
        dismissWithReason:IPHDismissalReasonType::
                              kTappedOutsideIPHAndAnchorView];
  }
  if (IsPinnedTabsEnabled()) {
    const BOOL pinnedTabsAvailable =
        currentPage == TabGridPage::TabGridPageRegularTabs &&
        _mode == TabGridMode::kNormal;
    [self.pinnedTabsViewController pinnedTabsAvailable:pinnedTabsAvailable];
  }
  [self updateToolbarsAppearance];
  // Make sure the current page becomes the first responder, so that it can
  // register and handle key commands.
  [self.currentPageViewController becomeFirstResponder];
}

// Sets the value of `currentPage`, adjusting the position of the scroll view
// to match. If `animated` is YES, the scroll view change may animate; if it is
// NO, it will never animate.
- (void)scrollToPage:(TabGridPage)targetPage animated:(BOOL)animated {
  // This method should never early return if `targetPage` == `_currentPage`;
  // the ivar may have been set before the scroll view could be updated. Calling
  // this method should always update the scroll view's offset if possible.

  // When VoiceOver is running, the animation can cause state to get out of
  // sync. If the user swipes right during the animation, the VoiceOver cursor
  // goes to the old page, instead of the new page. See crbug.com/978673 for
  // more details.
  if (UIAccessibilityIsVoiceOverRunning()) {
    animated = NO;
  }

  // If the view isn't loaded yet, just do bookkeeping on `currentPage`.
  if (!self.viewLoaded) {
    self.currentPage = targetPage;
    return;
  }

  CGFloat pageWidth = self.scrollView.frame.size.width;
  NSUInteger pageIndex = GetPageIndexFromPage(targetPage);
  CGPoint targetOffset = CGPointMake(pageIndex * pageWidth, 0);
  BOOL changed = self.currentPage != targetPage;
  BOOL scrolled =
      !CGPointEqualToPoint(self.scrollView.contentOffset, targetOffset);

  // If the view is visible and `animated` is YES, animate the change.
  // Otherwise don't.
  if (!self.viewVisible || !animated) {
    [self.scrollView setContentOffset:targetOffset animated:NO];
    self.currentPage = targetPage;
    // Important updates (e.g., button configurations, incognito visibility) are
    // made at the end of scrolling animations after `self.currentPage` is set.
    // Since this codepath has no animations, updates must be called manually.
    [self broadcastIncognitoContentVisibility];
  } else {
    // Only set `scrollViewAnimatingContentOffset` to YES if there's an actual
    // change in the contentOffset, as `-scrollViewDidEndScrollingAnimation:` is
    // never called if the animation does not occur.
    if (scrolled) {
      self.scrollViewAnimatingContentOffset = YES;
      [self.scrollView setContentOffset:targetOffset animated:YES];
      // `self.currentPage` is set in scrollViewDidEndScrollingAnimation:
    } else {
      self.currentPage = targetPage;
      if (changed) {
        // When there is no scrolling and the page changed, it can be due to
        // the user dragging the slider and dropping it right on the spot.
        // Something easy to reproduce with the two edges (incognito / recent
        // tabs), but also possible with middle position (normal).
        [self broadcastIncognitoContentVisibility];
      }
    }
  }

  // TODO(crbug.com/41406890) : This is a workaround because TabRestoreService
  // does not notify observers when entries are removed. When close all tabs
  // removes entries, the remote tabs page in the tab grid are not updated. This
  // ensures that the table is updated whenever scrolling to it.
  if (targetPage == TabGridPageRemoteTabs && (changed || scrolled)) {
    [self.remoteTabsViewController loadModel];
    [self.remoteTabsViewController.tableView reloadData];
  }
}

- (UIViewController*)currentPageViewController {
  switch (self.currentPage) {
    case TabGridPageIncognitoTabs:
      return self.incognitoTabsViewController
                 ? self.incognitoTabsViewController
                 : self.incognitoDisabledGridViewController;
    case TabGridPageRegularTabs:
      return self.regularTabsViewController
                 ? self.regularTabsViewController
                 : self.regularDisabledGridViewController;
    case TabGridPageRemoteTabs:
      return self.remoteTabsViewController ? self.remoteTabsViewController
                                           : self.remoteDisabledViewController;
    case TabGridPage::TabGridPageTabGroups:
      return self.tabGroupsPanelViewController
                 ? self.tabGroupsPanelViewController
                 : self.tabGroupsDisabledGridViewController;
  }
}

- (void)setScrollViewAnimatingContentOffset:
    (BOOL)scrollViewAnimatingContentOffset {
  if (_scrollViewAnimatingContentOffset == scrollViewAnimatingContentOffset)
    return;
  _scrollViewAnimatingContentOffset = scrollViewAnimatingContentOffset;
}

// Adds the scroll view and its content and sets constraints.
- (void)setupScrollView {
  UIScrollView* scrollView = [[UIScrollView alloc] init];
  scrollView.translatesAutoresizingMaskIntoConstraints = NO;
  scrollView.pagingEnabled = YES;
  scrollView.delegate = self;
  // Ensures that scroll view does not add additional margins based on safe
  // areas.
  scrollView.contentInsetAdjustmentBehavior =
      UIScrollViewContentInsetAdjustmentNever;

  [self addChildViewController:self.incognitoGridContainerViewController];
  [self addChildViewController:self.regularGridContainerViewController];
  UIViewController* thirdPanelGridContainerViewController =
      IsTabGroupSyncEnabled() ? self.tabGroupsGridContainerViewController
                              : self.remoteGridContainerViewController;
  [self addChildViewController:thirdPanelGridContainerViewController];
  UIStackView* gridsStack = [[UIStackView alloc] initWithArrangedSubviews:@[
    self.incognitoGridContainerViewController.view,
    self.regularGridContainerViewController.view,
    thirdPanelGridContainerViewController.view
  ]];
  gridsStack.translatesAutoresizingMaskIntoConstraints = NO;
  gridsStack.distribution = UIStackViewDistributionEqualSpacing;

  [scrollView addSubview:gridsStack];
  [self.view addSubview:scrollView];
  [self.incognitoGridContainerViewController
      didMoveToParentViewController:self];
  [self.regularGridContainerViewController didMoveToParentViewController:self];
  [thirdPanelGridContainerViewController didMoveToParentViewController:self];

  self.scrollView = scrollView;
  self.scrollView.scrollEnabled = YES;
  self.scrollView.accessibilityIdentifier = kTabGridScrollViewIdentifier;
  NSArray* constraints = @[
    [self.incognitoGridContainerViewController.view.widthAnchor
        constraintEqualToAnchor:self.view.widthAnchor],
    [self.regularGridContainerViewController.view.widthAnchor
        constraintEqualToAnchor:self.view.widthAnchor],
    [thirdPanelGridContainerViewController.view.widthAnchor
        constraintEqualToAnchor:self.view.widthAnchor],

    [scrollView.topAnchor constraintEqualToAnchor:self.view.topAnchor],
    [scrollView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
    [scrollView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
    [scrollView.trailingAnchor
        constraintEqualToAnchor:self.view.trailingAnchor],

    [gridsStack.topAnchor constraintEqualToAnchor:scrollView.topAnchor],
    [gridsStack.bottomAnchor constraintEqualToAnchor:scrollView.bottomAnchor],
    [gridsStack.leadingAnchor constraintEqualToAnchor:scrollView.leadingAnchor],
    [gridsStack.trailingAnchor
        constraintEqualToAnchor:scrollView.trailingAnchor],
    [gridsStack.heightAnchor constraintEqualToAnchor:scrollView.heightAnchor],
  ];
  [NSLayoutConstraint activateConstraints:constraints];
}

// Setup remote grid.
// TODO(crbug.com/40273478): Move this to the grid itself when specific grid
// file will be created.
- (void)setupRemoteTabsViewController {
  CHECK(!IsTabGroupSyncEnabled());
  self.remoteTabsViewController.UIDelegate = self;
  // TODO(crbug.com/41366321) : Dark style on remote tabs.
  // The styler must be set before the view controller is loaded.
  ChromeTableViewStyler* styler = [[ChromeTableViewStyler alloc] init];
  styler.tableViewBackgroundColor = [UIColor colorNamed:kGridBackgroundColor];
  self.remoteTabsViewController.overrideUserInterfaceStyle =
      UIUserInterfaceStyleDark;
  self.remoteTabsViewController.styler = styler;
  self.remoteGridContainerViewController.containedViewController =
      self.remoteTabsViewController;
  self.remoteTabsViewController.view.accessibilityElementsHidden =
      _currentPage != TabGridPageRemoteTabs;
}

// Adds a DisabledGridViewController as a contained view controller for the
// remote tabs.
- (void)setupDisabledRemoteTabsViewController {
  CHECK(!IsTabGroupSyncEnabled());
  self.remoteGridContainerViewController.containedViewController =
      self.remoteDisabledViewController;
  self.remoteDisabledViewController.delegate = self;
  self.remoteDisabledViewController.view.accessibilityElementsHidden =
      _currentPage != TabGridPageRemoteTabs;
}

// Adds the top toolbar and sets constraints.
- (void)setupTopToolbar {
  UIView* topToolbar = self.topToolbar;
  CHECK(topToolbar);

  [self.view addSubview:topToolbar];

  [NSLayoutConstraint activateConstraints:@[
    [topToolbar.topAnchor
        constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
    [topToolbar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
    [topToolbar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor]
  ]];
}

// Adds the bottom toolbar and sets constraints.
- (void)setupBottomToolbar {
  UIView* bottomToolbar = self.bottomToolbar;
  CHECK(bottomToolbar);

  [self.view addSubview:bottomToolbar];

  [NSLayoutConstraint activateConstraints:@[
    [bottomToolbar.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
    [bottomToolbar.leadingAnchor
        constraintEqualToAnchor:self.view.leadingAnchor],
    [bottomToolbar.trailingAnchor
        constraintEqualToAnchor:self.view.trailingAnchor],
  ]];

  [self.layoutGuideCenter referenceView:bottomToolbar
                              underName:kTabGridBottomToolbarGuide];
}

// Adds the PinnedTabsViewController and sets constraints.
- (void)setupPinnedTabsViewController {
  PinnedTabsViewController* pinnedTabsViewController =
      self.pinnedTabsViewController;
  pinnedTabsViewController.delegate = self;

  [self addChildViewController:pinnedTabsViewController];
  [self.view addSubview:pinnedTabsViewController.view];
  [pinnedTabsViewController didMoveToParentViewController:self];

  [self updatePinnedTabsViewControllerConstraints];
}

- (void)configureViewControllerForCurrentSizeClassesAndPage {
  self.configuration = TabGridConfigurationFloatingButton;
  if ([self shouldUseCompactLayout] || _mode == TabGridMode::kSelection) {
    // The bottom toolbar configuration is applied when the UI is narrow but
    // vertically long or the selection mode is enabled.
    self.configuration = TabGridConfigurationBottomToolbar;
  }
}

// Shows the two toolbars and the floating button. Suitable for use in
// animations.
- (void)showToolbars {
  [self.topToolbar show];
  [self.bottomToolbar show];
}

// Hides the two toolbars. Suitable for use in animations.
- (void)hideToolbars {
  [self.topToolbar hide];
  [self.bottomToolbar hide];
}

// Translates the toolbar views offscreen and then animates them back in using
// the transition coordinator. Transitions are preferred here since they don't
// interact with the layout system at all.
- (void)animateToolbarsForAppearance {
  DCHECK(self.transitionCoordinator);
  // Unless reduce motion is enabled, hide the scroll view during the
  // animation.
  if (!UIAccessibilityIsReduceMotionEnabled()) {
    self.scrollView.hidden = YES;
  }
  // Fade the toolbars in for the last 60% of the transition.
  auto keyframe = ^{
    [UIView addKeyframeWithRelativeStartTime:0.2
                            relativeDuration:0.6
                                  animations:^{
                                    [self showToolbars];
                                  }];
  };
  // Animation block that does the keyframe animation.
  auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
    [UIView animateKeyframesWithDuration:context.transitionDuration
                                   delay:0
                                 options:UIViewAnimationOptionLayoutSubviews
                              animations:keyframe
                              completion:nil];
  };

  // Restore the scroll view and toolbar opacities (in case the animation didn't
  // complete) as part of the completion.
  auto cleanup = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
    self.scrollView.hidden = NO;
    [self showToolbars];
  };

  // Animate the toolbar alphas alongside the current transition.
  [self.transitionCoordinator animateAlongsideTransition:animation
                                              completion:cleanup];
}

// Translates the toolbar views offscreen using the transition coordinator.
- (void)animateToolbarsForDisappearance {
  DCHECK(self.transitionCoordinator);
  // Unless reduce motion is enabled, hide the scroll view during the
  // animation.
  if (!UIAccessibilityIsReduceMotionEnabled()) {
    self.scrollView.hidden = YES;
  }
  // Fade the toolbars out in the first 66% of the transition.
  auto keyframe = ^{
    [UIView addKeyframeWithRelativeStartTime:0
                            relativeDuration:0.40
                                  animations:^{
                                    [self hideToolbars];
                                  }];
  };

  // Animation block that does the keyframe animation.
  auto animation = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
    [UIView animateKeyframesWithDuration:context.transitionDuration
                                   delay:0
                                 options:UIViewAnimationOptionLayoutSubviews
                              animations:keyframe
                              completion:nil];
  };

  // Hide the scroll view (and thus the tab grids) until the transition
  // completes. Restore the toolbar opacity when the transition completes.
  auto cleanup = ^(id<UIViewControllerTransitionCoordinatorContext> context) {
    self.scrollView.hidden = NO;
  };

  // Animate the toolbar alphas alongside the current transition.
  [self.transitionCoordinator animateAlongsideTransition:animation
                                              completion:cleanup];
}

// Tells the appropriate delegate to create a new item, and then tells the
// presentation delegate to show the new item.
- (void)openNewTabInPage:(TabGridPage)page focusOmnibox:(BOOL)focusOmnibox {
  // Guard against opening new tabs in a page that is disabled. It is the job
  // of the caller to make sure to not open a new tab in a page that can't
  // perform the action. For example, it is an error to attempt to open a new
  // tab in the incognito page when incognito is disabled by policy.
  CHECK([self canPerformOpenNewTabActionForDestinationPage:page]);

  switch (page) {
    case TabGridPageIncognitoTabs:
      [self.incognitoTabsViewController prepareForDismissal];
      [self.incognitoGridHandler addNewItem];
      break;
    case TabGridPageRegularTabs:
      [self.regularTabsViewController prepareForDismissal];
      [self.regularGridHandler addNewItem];
      break;
    case TabGridPageRemoteTabs:
      NOTREACHED_IN_MIGRATION()
          << "It is invalid to open a new tab in Recent Tabs.";
      break;
    case TabGridPageTabGroups:
      NOTREACHED_IN_MIGRATION()
          << "It is invalid to open a new tab in Tab Groups.";
      break;
  }
  self.activePage = page;
  [self.tabPresentationDelegate showActiveTabInPage:page
                                       focusOmnibox:focusOmnibox];
}

// Creates and shows a new regular tab.
- (void)openNewRegularTabForKeyboardCommand {
  [self.handler dismissModalDialogsWithCompletion:nil];
  [self openNewTabInPage:TabGridPageRegularTabs focusOmnibox:YES];
  base::RecordAction(
      base::UserMetricsAction("MobileTabGridCreateRegularTabKeyboard"));
}

// Creates and shows a new incognito tab.
- (void)openNewIncognitoTabForKeyboardCommand {
  [self.handler dismissModalDialogsWithCompletion:nil];
  [self openNewTabInPage:TabGridPageIncognitoTabs focusOmnibox:YES];
  base::RecordAction(
      base::UserMetricsAction("MobileTabGridCreateIncognitoTabKeyboard"));
}

// Creates and shows a new tab in the current page.
- (void)openNewTabInCurrentPageForKeyboardCommand {
  switch (self.currentPage) {
    case TabGridPageIncognitoTabs:
      [self openNewIncognitoTabForKeyboardCommand];
      break;
    case TabGridPageRegularTabs:
      [self openNewRegularTabForKeyboardCommand];
      break;
    case TabGridPageRemoteTabs:
      NOTREACHED_IN_MIGRATION()
          << "It is invalid to open a new tab from Recent Tabs.";
      break;
    case TabGridPageTabGroups:
      NOTREACHED_IN_MIGRATION()
          << "It is invalid to open a new tab from Tab Groups.";
      break;
  }
}

// Broadcasts whether incognito tabs are showing.
- (void)broadcastIncognitoContentVisibility {
  // It is programmer error to broadcast incognito content visibility when the
  // view is not visible.
  if (!self.viewVisible)
    return;
  BOOL incognitoContentVisible =
      (self.currentPage == TabGridPageIncognitoTabs &&
       !self.incognitoTabsViewController.gridEmpty);
  [self.handler setIncognitoContentVisible:incognitoContentVisible];
}

- (void)setupSearchUI {
  self.scrimView = [[UIControl alloc] init];
  self.scrimView.backgroundColor =
      [UIColor colorNamed:kDarkerScrimBackgroundColor];
  self.scrimView.translatesAutoresizingMaskIntoConstraints = NO;
  self.scrimView.accessibilityIdentifier = kTabGridScrimIdentifier;
  [self.scrimView addTarget:self
                     action:@selector(quitSearchMode)
           forControlEvents:UIControlEventTouchUpInside];
  // Add a gesture recognizer to identify when the user interactions with the
  // search results.
  self.searchResultPanRecognizer =
      [[UIPanGestureRecognizer alloc] initWithTarget:self.view
                                              action:@selector(endEditing:)];
  self.searchResultPanRecognizer.cancelsTouchesInView = NO;
  self.searchResultPanRecognizer.delegate = self;
}

// Shows scrim overlay.
- (void)showScrim {
  self.scrimView.alpha = 0.0f;
  self.scrimView.hidden = NO;
  if (!self.scrimView.superview) {
    [self.scrollView addSubview:self.scrimView];
    AddSameConstraints(self.scrimView, self.view.superview);
    [self.view layoutIfNeeded];
  }
  self.currentPageViewController.accessibilityElementsHidden = YES;
  __weak __typeof(self) weakSelf = self;
  [UIView animateWithDuration:kAnimationDuration.InSecondsF()
      animations:^{
        TabGridViewController* strongSelf = weakSelf;
        if (!strongSelf)
          return;
        strongSelf.scrimView.alpha = 1.0f;
      }
      completion:^(BOOL finished) {
        TabGridViewController* strongSelf = weakSelf;
        if (!strongSelf)
          return;
        strongSelf.currentPageViewController.accessibilityElementsHidden = YES;
      }];
}

// Hides scrim overlay.
- (void)hideScrim {
  __weak TabGridViewController* weakSelf = self;
  [UIView animateWithDuration:kAnimationDuration.InSecondsF()
      animations:^{
        TabGridViewController* strongSelf = weakSelf;
        if (!strongSelf)
          return;

        strongSelf.scrimView.alpha = 0.0f;
      }
      completion:^(BOOL finished) {
        TabGridViewController* strongSelf = weakSelf;
        if (!strongSelf)
          return;
        strongSelf.scrimView.hidden = YES;
        strongSelf.currentPageViewController.accessibilityElementsHidden = NO;
      }];
}

// Updates the appearance of the toolbars based on the scroll position of the
// currently active Grid.
- (void)updateToolbarsAppearance {
  BOOL gridScrolledToTop;
  BOOL gridScrolledToBottom;
  switch (self.currentPage) {
    case TabGridPageIncognitoTabs:
      gridScrolledToTop = self.incognitoTabsViewController.scrolledToTop;
      gridScrolledToBottom = self.incognitoTabsViewController.scrolledToBottom;
      break;
    case TabGridPageRegularTabs:
      gridScrolledToTop = self.regularTabsViewController.scrolledToTop;
      gridScrolledToBottom = self.regularTabsViewController.scrolledToBottom;
      break;
    case TabGridPageRemoteTabs:
      gridScrolledToTop = self.remoteTabsViewController.scrolledToTop;
      gridScrolledToBottom = self.remoteTabsViewController.scrolledToBottom;
      break;
    case TabGridPage::TabGridPageTabGroups:
      gridScrolledToTop = self.tabGroupsPanelViewController.scrolledToTop;
      gridScrolledToBottom = self.tabGroupsPanelViewController.scrolledToBottom;
      break;
  }
  [self.topToolbar setScrollViewScrolledToEdge:gridScrolledToTop];
  [self.bottomToolbar setScrollViewScrolledToEdge:gridScrolledToBottom];
}

- (void)reportTabSelectionTime {
  if (self.tabGridEnterTime.is_null()) {
    // The enter time was not recorded. Bail out.
    return;
  }
  base::TimeDelta duration = base::TimeTicks::Now() - self.tabGridEnterTime;
  base::UmaHistogramLongTimes("IOS.TabSwitcher.TimeSpentOpeningExistingTab",
                              duration);
  self.tabGridEnterTime = base::TimeTicks();
}

// Returns YES if the switcher page is enabled. For example, the page may be
// disabled by policy, in which case NO is returned.
- (BOOL)isPageEnabled:(TabGridPage)page {
  switch (page) {
    case TabGridPageIncognitoTabs:
      return _pageConfiguration !=
             TabGridPageConfiguration::kIncognitoPageDisabled;
    case TabGridPageRegularTabs:
    case TabGridPageRemoteTabs:
    case TabGridPageTabGroups:
      return _pageConfiguration != TabGridPageConfiguration::kIncognitoPageOnly;
  }
}

// Returns YES if a new tab action that targets the `destinationPage` can be
// performed. The _currentPage can be the same page as the `destinationPage`.
- (BOOL)canPerformOpenNewTabActionForDestinationPage:
    (TabGridPage)destinationPage {
  return [self isPageEnabled:destinationPage] &&
         self.currentPage != TabGridPageRemoteTabs &&
         self.currentPage != TabGridPageTabGroups;
}

// Returns transition layout for the provided `page`.
- (TabGridTransitionItem*)transitionItemForActiveCellWithActivePage:
    (TabGridPage)activePage {
  switch (activePage) {
    case TabGridPageIncognitoTabs:
      return [self.incognitoTabsViewController transitionItemForActiveCell];
    case TabGridPageRegularTabs:
      return [self transitionItemForRegularActiveCell];
    case TabGridPageRemoteTabs:
    case TabGridPageTabGroups:
      return nil;
  }
}

// Returns transition layout provider for the regular tabs page.
- (TabGridTransitionItem*)transitionItemForRegularActiveCell {
  if (IsPinnedTabsEnabled() && self.pinnedTabsViewController.hasSelectedCell) {
    return [self.pinnedTabsViewController transitionItemForActiveCell];
  }

  return [self.regularTabsViewController transitionItemForActiveCell];
}

// Quit search mode.
- (void)quitSearchMode {
  [self.mutator quitSearchMode];
}

// Optionally presents a full screen IPH that instructs the user to right swipe
// to view the incognito tab grid. If the delegate determines that the user
// supposed to see this tip, and the IPH fits on the current screen both
// contextually and visually, then it initializes `swipeToIncognitoIPH` and
// presents a GestureInProductHelpView. Otherwise, it keeps
// `swipeToIncognitoIPH` to `nil` and no gestural tip is shown.
- (void)maybeShowSwipeToIncognitoIPH {
  // Return if the regular tabs are visible.
  if (!self.viewVisible || self.currentPage != TabGridPageRegularTabs) {
    return;
  }
  // Check whether the user should see the IPH.
  if (![self.delegate tabGridIsUserEligibleForSwipeToIncognitoIPH]) {
    return;
  }
  // Return if the IPH has already been presented.
  if (self.swipeToIncognitoIPH) {
    return;
  }

  // Create the view.
  UIView* regularGridView = self.regularTabsViewController.view;
  CGSize expectedSize = CGSize();
  CGFloat expectedHeight =
      regularGridView.frame.size.height - self.topToolbar.bounds.size.height;
  expectedHeight -=
      self.view.window.windowScene.statusBarManager.statusBarFrame.size.height;
  if ([self shouldUseCompactLayout]) {
    expectedHeight -= self.bottomToolbar.bounds.size.height;
  }
  expectedSize.height = expectedHeight;
  CGFloat safeAreaInsetForArrowDirection =
      UseRTLLayout() ? regularGridView.safeAreaInsets.right
                     : regularGridView.safeAreaInsets.left;
  expectedSize.width =
      regularGridView.frame.size.width - safeAreaInsetForArrowDirection;

  int stringID = IDS_IOS_SWIPE_RIGHT_TO_INCOGNITO_IPH;
  int voiceOverAnnouncementStringID =
      IDS_IOS_SWIPE_RIGHT_TO_INCOGNITO_IPH_VOICEOVER;
  UISwipeGestureRecognizerDirection swipeDirection =
      UISwipeGestureRecognizerDirectionRight;
  if (UseRTLLayout()) {
    stringID = IDS_IOS_SWIPE_LEFT_TO_INCOGNITO_IPH;
    voiceOverAnnouncementStringID =
        IDS_IOS_SWIPE_LEFT_TO_INCOGNITO_IPH_VOICEOVER;
    swipeDirection = UISwipeGestureRecognizerDirectionLeft;
  }
  GestureInProductHelpView* gestureIPHView = [[GestureInProductHelpView alloc]
               initWithText:l10n_util::GetNSString(stringID)
         bubbleBoundingSize:expectedSize
             swipeDirection:swipeDirection
      voiceOverAnnouncement:l10n_util::GetNSString(
                                voiceOverAnnouncementStringID)];
  [gestureIPHView setTranslatesAutoresizingMaskIntoConstraints:NO];

  // Return if the view does NOT fit in the regular tab grid.
  CGSize smallestPossibleSizeOfIPH = [gestureIPHView
      systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
  if (smallestPossibleSizeOfIPH.width > expectedSize.width ||
      smallestPossibleSizeOfIPH.height > expectedSize.height) {
    return;
  }
  if (![self.delegate tabGridShouldPresentSwipeToIncognitoIPH]) {
    return;
  }
  gestureIPHView.delegate = self;
  self.swipeToIncognitoIPH = gestureIPHView;
  [self.view addSubview:self.swipeToIncognitoIPH];
  self.swipeToIncognitoIPHBottomConstraint = [gestureIPHView.bottomAnchor
      constraintEqualToAnchor:[self shouldUseCompactLayout]
                                  ? self.bottomToolbar.topAnchor
                                  : regularGridView.bottomAnchor];
  [NSLayoutConstraint activateConstraints:@[
    [gestureIPHView.leadingAnchor
        constraintEqualToAnchor:regularGridView.leadingAnchor],
    [gestureIPHView.trailingAnchor
        constraintEqualToAnchor:regularGridView.trailingAnchor],
    [gestureIPHView.topAnchor
        constraintEqualToAnchor:self.topToolbar.bottomAnchor],
    self.swipeToIncognitoIPHBottomConstraint
  ]];
  [self.swipeToIncognitoIPH startAnimation];
}

// Called when a drag will begin.
- (void)dragSessionWillBegin {
  self.dragSessionInProgress = YES;
  [self.mutator dragAndDropSessionStarted];

  // Actions on both bars should be disabled during dragging.
  self.topToolbar.pageControl.userInteractionEnabled = NO;
}

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
    shouldRecognizeSimultaneouslyWithGestureRecognizer:
        (UIGestureRecognizer*)otherGestureRecognizer {
  if (gestureRecognizer == self.searchResultPanRecognizer)
    return YES;
  return NO;
}

#pragma mark - UISearchBarDelegate

- (void)searchBarTextDidBeginEditing:(UISearchBar*)searchBar {
  _searchText = searchBar.text;
  [self updateScrimVisibilityForText:searchBar.text];
  [self.currentPageViewController.view
      addGestureRecognizer:self.searchResultPanRecognizer];
}

- (void)searchBarTextDidEndEditing:(UISearchBar*)searchBar {
  [self.currentPageViewController.view
      removeGestureRecognizer:self.searchResultPanRecognizer];
}

- (void)searchBarSearchButtonClicked:(UISearchBar*)searchBar {
  [searchBar resignFirstResponder];
}

- (void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)searchText {
  if ([_searchText isEqualToString:searchText]) {
    // It seems that in some cases, the keyboard is triggered twice in the same
    // runloop. This is a tentative fix to avoid trigger duplicate updates. See
    // crbug.com/336515391.
    return;
  }
  _searchText = searchText;
  searchBar.searchTextField.accessibilityIdentifier =
      [kTabGridSearchTextFieldIdentifierPrefix
          stringByAppendingString:searchText];
  [self updateScrimVisibilityForText:searchText];
  switch (self.currentPage) {
    case TabGridPageIncognitoTabs:
      self.incognitoTabsViewController.searchText = searchText;
      [self updateSearchGrid:self.incognitoGridHandler
              withSearchText:searchText];
      break;
    case TabGridPageRegularTabs:
      self.regularTabsViewController.searchText = searchText;
      [self updateSearchGrid:self.regularGridHandler withSearchText:searchText];
      break;
    case TabGridPageRemoteTabs:
      self.remoteTabsViewController.searchTerms = searchText;
      break;
    case TabGridPage::TabGridPageTabGroups:
      NOTREACHED_IN_MIGRATION() << "Tab Groups doesn't support searching";
      break;
  }
}

- (void)updateSearchGrid:(id<GridCommands>)tabsDelegate
          withSearchText:(NSString*)searchText {
  if (searchText.length) {
    [tabsDelegate searchItemsWithText:searchText];
  } else {
    // The expectation from searchItemsWithText is to search tabs from all
    // the available windows to the app. However in the case of empy string
    // the grid should revert back to its original state so it doesn't
    // display all the tabs from all the available windows.
    [tabsDelegate resetToAllItems];
  }
}

- (void)updateScrimVisibilityForText:(NSString*)searchText {
  if (_mode != TabGridMode::kSearch) {
    return;
  }
  if (searchText.length == 0) {
    self.isPerformingSearch = NO;
    [self showScrim];
  } else if (!self.isPerformingSearch) {
    self.isPerformingSearch = YES;
    // If no results have been presented yet, then hide the scrim to present
    // the results.
    [self hideScrim];
  }
}

// Calculates the proper insets for a Tab Grid panel to accommodate for the safe
// area and toolbar.
- (UIEdgeInsets)calculateInsetsForGridView {
  // The content inset of the tab grids must be modified so that the toolbars
  // do not obscure the tabs. This may change depending on orientation.
  CGFloat bottomInset = self.configuration == TabGridConfigurationBottomToolbar
                            ? self.bottomToolbar.intrinsicContentSize.height
                            : 0;

  CGFloat topInset = self.topToolbar.intrinsicContentSize.height;
  UIEdgeInsets inset = UIEdgeInsetsMake(topInset, 0, bottomInset, 0);
  inset.left = self.scrollView.safeAreaInsets.left;
  inset.right = self.scrollView.safeAreaInsets.right;
  inset.top += self.scrollView.safeAreaInsets.top;
  inset.bottom += self.scrollView.safeAreaInsets.bottom;

  return inset;
}

// Calculates the proper insets for the Regular Grid ViewController to
// accommodate for the safe area and toolbars. It differs from
// `calculateInsetsForGridView` when there is the Pinned Tabs tray to account
// for as well.
- (UIEdgeInsets)calculateInsetsForRegularGridView {
  UIEdgeInsets inset = [self calculateInsetsForGridView];

  if (IsPinnedTabsEnabled() && self.pinnedTabsViewController.visible) {
    CGFloat pinnedViewHeight =
        self.pinnedTabsViewController.view.bounds.size.height;
    inset.bottom += pinnedViewHeight + kPinnedViewBottomPadding;
  }

  return inset;
}

#pragma mark - RecentTabsTableViewControllerUIDelegate

- (void)recentTabsScrollViewDidScroll:
    (RecentTabsTableViewController*)recentTabsTableViewController {
  [self updateToolbarsAppearance];
}

#pragma mark - TabGroupsPanelViewControllerUIDelegate

- (void)tabGroupsPanelViewControllerDidScroll:
    (TabGroupsPanelViewController*)tabGroupsPanelViewController {
  [self updateToolbarsAppearance];
}

#pragma mark - PinnedTabsViewControllerDelegate

- (void)pinnedTabsViewController:
            (PinnedTabsViewController*)pinnedTabsViewController
             didSelectItemWithID:(web::WebStateID)itemID {
  base::RecordAction(base::UserMetricsAction("MobileTabGridPinnedTabSelected"));
  // Record how long it took to select an item.
  [self reportTabSelectionTime];

  [self.regularGridHandler selectItemWithID:itemID
                                     pinned:YES
                     isFirstActionOnTabGrid:[self status]];

  self.activePage = self.currentPage;
  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];

  [self.tabPresentationDelegate showActiveTabInPage:self.currentPage
                                       focusOmnibox:NO];
}

- (void)pinnedTabsViewControllerVisibilityDidChange:
    (PinnedTabsViewController*)pinnedTabsViewController {
  UIEdgeInsets insets = [self calculateInsetsForRegularGridView];
  [UIView animateWithDuration:kPinnedViewInsetAnimationTime
                   animations:^{
                     self.regularTabsViewController.contentInsets = insets;
                   }];
}

- (void)pinnedTabsViewControllerDidMoveItem:
    (PinnedTabsViewController*)pinnedTabsViewController {
  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
}

- (void)pinnedTabsViewController:(BaseGridViewController*)gridViewController
             didRemoveItemWIthID:(web::WebStateID)itemID {
  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
}

- (void)pinnedViewControllerDropAnimationWillBegin:
    (PinnedTabsViewController*)pinnedTabsViewController {
  self.regularTabsViewController.dropAnimationInProgress = YES;
}

- (void)pinnedViewControllerDropAnimationDidEnd:
    (PinnedTabsViewController*)pinnedTabsViewController {
  self.regularTabsViewController.dropAnimationInProgress = NO;
}

- (void)pinnedViewControllerDragSessionWillBegin:
    (PinnedTabsViewController*)pinnedTabsViewController {
  self.dragSessionInProgress = YES;
  [self.mutator dragAndDropSessionStarted];
}

- (void)pinnedViewControllerDragSessionDidEnd:
    (PinnedTabsViewController*)pinnedTabsViewController {
  self.dragSessionInProgress = NO;
  [self.mutator dragAndDropSessionEnded];
}

- (void)pinnedViewControllerDidRequestContextMenu:
    (PinnedTabsViewController*)pinnedTabsViewController {
  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
}

#pragma mark - GridViewControllerDelegate

- (void)gridViewController:(BaseGridViewController*)gridViewController
       didSelectItemWithID:(web::WebStateID)itemID {
  // Check that the current page matches the grid view being interacted with.
  BOOL isOnRegularTabsPage = self.currentPage == TabGridPageRegularTabs;
  BOOL isOnIncognitoTabsPage = self.currentPage == TabGridPageIncognitoTabs;
  BOOL isOnThirdPanel = self.currentPage == TabGridPageRemoteTabs ||
                        self.currentPage == TabGridPageTabGroups;
  BOOL gridIsRegularTabs = gridViewController == self.regularTabsViewController;
  BOOL gridIsIncognitoTabs =
      gridViewController == self.incognitoTabsViewController;
  if ((isOnRegularTabsPage && !gridIsRegularTabs) ||
      (isOnIncognitoTabsPage && !gridIsIncognitoTabs) || isOnThirdPanel) {
    return;
  }

  if (_mode == TabGridMode::kSelection) {
    return;
  }

  id<GridCommands> tabsDelegate;
  if (gridViewController == self.regularTabsViewController) {
    tabsDelegate = self.regularGridHandler;
    base::RecordAction(base::UserMetricsAction("MobileTabGridOpenRegularTab"));
    if (_mode == TabGridMode::kSearch) {
      base::RecordAction(
          base::UserMetricsAction("MobileTabGridOpenRegularTabSearchResult"));
    }
  } else if (gridViewController == self.incognitoTabsViewController) {
    tabsDelegate = self.incognitoGridHandler;
    base::RecordAction(
        base::UserMetricsAction("MobileTabGridOpenIncognitoTab"));
    if (_mode == TabGridMode::kSearch) {
      base::RecordAction(
          base::UserMetricsAction("MobileTabGridOpenIncognitoTabSearchResult"));
    }
  }
  // Record how long it took to select an item.
  [self reportTabSelectionTime];

  // Check if the tab being selected is already selected.
  BOOL alreadySelected = [tabsDelegate isItemWithIDSelected:itemID];

  [tabsDelegate selectItemWithID:itemID
                          pinned:NO
          isFirstActionOnTabGrid:[self status]];

  if (!alreadySelected) {
    [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
  }

  if (_mode == TabGridMode::kSearch) {
    if (![tabsDelegate isItemWithIDSelected:itemID]) {
      // That can happen when the search result that was selected is from
      // another window. In that case don't change the active page for this
      // window and don't show the tab group view.
      base::RecordAction(base::UserMetricsAction(
          "MobileTabGridOpenTabGroupSearchResultInAnotherWindow"));
      return;
    } else {
      // Make sure that the keyboard is dismissed before starting the transition
      // to the selected tab.
      [self.view endEditing:YES];
    }
  }

  [self.tabPresentationDelegate showActiveTabInPage:self.currentPage
                                       focusOmnibox:NO];
}

- (void)gridViewController:(BaseGridViewController*)gridViewController
            didSelectGroup:(const TabGroup*)group {
  // Check that the current page matches the grid view being interacted with.
  BOOL isOnRegularTabsPage = self.currentPage == TabGridPageRegularTabs;
  BOOL isOnIncognitoTabsPage = self.currentPage == TabGridPageIncognitoTabs;
  BOOL isOnThirdPanel = self.currentPage == TabGridPageRemoteTabs ||
                        self.currentPage == TabGridPageTabGroups;
  BOOL gridIsRegularTabs = gridViewController == self.regularTabsViewController;
  BOOL gridIsIncognitoTabs =
      gridViewController == self.incognitoTabsViewController;
  if ((isOnRegularTabsPage && !gridIsRegularTabs) ||
      (isOnIncognitoTabsPage && !gridIsIncognitoTabs) || isOnThirdPanel) {
    return;
  }

  if (_mode == TabGridMode::kSelection) {
    return;
  }

  id<GridCommands> tabsDelegate;
  if (gridViewController == self.regularTabsViewController) {
    tabsDelegate = self.regularGridHandler;
    base::RecordAction(
        base::UserMetricsAction("MobileTabGridOpenRegularTabGroup"));
    if (_mode == TabGridMode::kSearch) {
      base::RecordAction(base::UserMetricsAction(
          "MobileTabGridOpenRegularTabGroupSearchResult"));
    }
  } else if (gridViewController == self.incognitoTabsViewController) {
    tabsDelegate = self.incognitoGridHandler;
    base::RecordAction(
        base::UserMetricsAction("MobileTabGridOpenIncognitoTabGroup"));
    if (_mode == TabGridMode::kSearch) {
      base::RecordAction(base::UserMetricsAction(
          "MobileTabGridOpenIncognitoTabGroupSearchResult"));
    }
  }

  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];

  [tabsDelegate selectTabGroup:group];

  if (_mode == TabGridMode::kSearch) {
    // Make sure that the keyboard is dismissed.
    [self.view endEditing:YES];
  }
}

// TODO(crbug.com/40273478): Remove once inactive tabs do not depends on it
// anymore.
- (void)gridViewController:(BaseGridViewController*)gridViewController
        didCloseItemWithID:(web::WebStateID)itemID {
  // No-op
}

- (void)gridViewControllerDidMoveItem:
    (BaseGridViewController*)gridViewController {
  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
}

- (void)gridViewController:(BaseGridViewController*)gridViewController
       didRemoveItemWIthID:(web::WebStateID)itemID {
  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
}

- (void)gridViewControllerDragSessionWillBeginForTab:
    (BaseGridViewController*)gridViewController {
  [self dragSessionWillBegin];
  if (IsPinnedTabsEnabled()) {
    [self.pinnedTabsViewController dragSessionEnabled:YES];
  }
}

- (void)gridViewControllerDragSessionWillBeginForTabGroup:
    (BaseGridViewController*)gridViewController {
  [self dragSessionWillBegin];
}

- (void)gridViewControllerDragSessionDidEnd:
    (BaseGridViewController*)gridViewController {
  self.dragSessionInProgress = NO;
  [self.mutator dragAndDropSessionEnded];
  self.topToolbar.pageControl.userInteractionEnabled = YES;

  if (IsPinnedTabsEnabled()) {
    [self.pinnedTabsViewController dragSessionEnabled:NO];
  }
}

- (void)gridViewControllerScrollViewDidScroll:
    (BaseGridViewController*)gridViewController {
  [self updateToolbarsAppearance];
}

- (void)gridViewControllerDropAnimationWillBegin:
    (BaseGridViewController*)gridViewController {
  if (IsPinnedTabsEnabled()) {
    self.pinnedTabsViewController.dropAnimationInProgress = YES;
  }
}

- (void)gridViewControllerDropAnimationDidEnd:
    (BaseGridViewController*)gridViewController {
  if (IsPinnedTabsEnabled()) {
    [self.pinnedTabsViewController dropAnimationDidEnd];
  }
}

- (void)didTapInactiveTabsButtonInGridViewController:
    (BaseGridViewController*)gridViewController {
  CHECK(IsInactiveTabsEnabled());
  if (self.currentPage != TabGridPageRegularTabs) {
    return;
  }
  base::RecordAction(base::UserMetricsAction("MobileTabGridShowInactiveTabs"));
  [self.delegate showInactiveTabs];
  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
}

- (void)didTapInactiveTabsSettingsLinkInGridViewController:
    (BaseGridViewController*)gridViewController {
  NOTREACHED_IN_MIGRATION();
}

- (void)gridViewControllerDidRequestContextMenu:
    (BaseGridViewController*)gridViewController {
  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
  // The searchBar must relinquish its status as first responder to become
  // interactable again.
  [self.topToolbar unfocusSearchBar];
}

#pragma mark - TabGridToolbarsMainTabGridDelegate

- (void)pageControlChangedValue:(id)sender {
  // Map the page control slider position (in the range 0.0-1.0) to an
  // x-offset for the scroll view.
  CGFloat offset = self.topToolbar.pageControl.sliderPosition;
  // In RTL, flip the offset.
  if (UseRTLLayout())
    offset = 1.0 - offset;

  // Total space available for the scroll view to scroll (horizontally).
  CGFloat offsetWidth =
      self.scrollView.contentSize.width - self.scrollView.frame.size.width;
  CGPoint contentOffset = self.scrollView.contentOffset;
  // Find the final offset by using `offset` as a fraction of the available
  // scroll width.
  contentOffset.x = offsetWidth * offset;
  self.scrollView.contentOffset = contentOffset;
}

- (void)pageControlChangedPageByDrag:(id)sender {
  TabGridPage newPage = self.topToolbar.pageControl.selectedPage;

  // Records when the user uses the pageControl to switch pages.
  if (self.currentPage != newPage) {
    [self.mutator pageChanged:newPage
                  interaction:TabSwitcherPageChangeInteraction::kControlDrag];
  }
  [self scrollToPage:newPage animated:YES];
}

- (void)pageControlChangedPageByTap:(id)sender {
  TabGridPage newPage = self.topToolbar.pageControl.selectedPage;

  // Records when the user uses the pageControl to switch pages.
  if (self.currentPage != newPage) {
    [self.mutator pageChanged:newPage
                  interaction:TabSwitcherPageChangeInteraction::kControlTap];
  }
  [self scrollToPage:newPage animated:YES];
}

#pragma mark - DisabledGridViewControllerDelegate

- (void)didTapLinkWithURL:(const GURL&)URL {
  [self.delegate openLinkWithURL:URL];
}

- (bool)isViewControllerSubjectToParentalControls {
  return _isSubjectToParentalControls;
}

#pragma mark - TabGridConsumer

- (void)updateParentalControlStatus:(BOOL)isSubjectToParentalControls {
  _isSubjectToParentalControls = isSubjectToParentalControls;
}

- (void)updateTabGridForIncognitoModeDisabled:(BOOL)isIncognitoModeDisabled {
  BOOL isTabGridUpdated = NO;

  if (isIncognitoModeDisabled &&
      _pageConfiguration == TabGridPageConfiguration::kAllPagesEnabled) {
    _pageConfiguration = TabGridPageConfiguration::kIncognitoPageDisabled;
    isTabGridUpdated = YES;
  } else if (!isIncognitoModeDisabled &&
             _pageConfiguration ==
                 TabGridPageConfiguration::kIncognitoPageDisabled) {
    _pageConfiguration = TabGridPageConfiguration::kAllPagesEnabled;
    isTabGridUpdated = YES;
  }

  if (isTabGridUpdated) {
    [self broadcastIncognitoContentVisibility];
  }
}

- (void)setMode:(TabGridMode)mode {
  if (_mode == mode) {
    return;
  }
  [self tabGridDidPerformAction:TabGridActionType::kInPageAction];
  if (self.swipeToIncognitoIPH) {
    [self.swipeToIncognitoIPH
        dismissWithReason:IPHDismissalReasonType::
                              kTappedOutsideIPHAndAnchorView];
  }

  TabGridMode previousMode = _mode;
  _mode = mode;

  if (previousMode == TabGridMode::kSearch) {
    self.remoteTabsViewController.searchTerms = nil;
    self.regularTabsViewController.searchText = nil;
    self.incognitoTabsViewController.searchText = nil;
    [self.regularGridHandler resetToAllItems];
    [self.incognitoGridHandler resetToAllItems];
    [self hideScrim];
  }

  [self setInsetForGridViews];
  self.scrollView.scrollEnabled = (_mode == TabGridMode::kNormal);
}

#pragma mark - UIResponder

// To always be able to register key commands via -keyCommands, the VC must be
// able to become first responder.
- (BOOL)canBecomeFirstResponder {
  return YES;
}

- (UIResponder*)nextResponder {
  UIResponder* nextResponder = [super nextResponder];
  if (self.viewVisible) {
    // Add toolbars to the responder chain.
    // TODO(crbug.com/40273478): Transform toolbars in view controller directly
    // have it in the chain by default instead of adding it manually.
    [self.bottomToolbar respondBeforeResponder:nextResponder];
    [self.topToolbar respondBeforeResponder:self.bottomToolbar];
    return self.topToolbar;
  } else {
    return nextResponder;
  }
}

- (NSArray<UIKeyCommand*>*)keyCommands {
  // On iOS 15+, key commands visible in the app's menu are created in
  // MenuBuilder. Return the key commands that are not already present in the
  // menu.
  return @[
    UIKeyCommand.cr_openNewRegularTab,
    // TODO(crbug.com/40246790): Move it to the menu builder once we have the
    // strings.
    UIKeyCommand.cr_select2,
    UIKeyCommand.cr_select3,
  ];
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
  if (sel_isEqual(action, @selector(keyCommand_openNewTab))) {
    return [self canPerformOpenNewTabActionForDestinationPage:self.currentPage];
  }
  if (sel_isEqual(action, @selector(keyCommand_openNewRegularTab))) {
    return [self
        canPerformOpenNewTabActionForDestinationPage:TabGridPageRegularTabs];
  }
  if (sel_isEqual(action, @selector(keyCommand_openNewIncognitoTab))) {
    return [self
        canPerformOpenNewTabActionForDestinationPage:TabGridPageIncognitoTabs];
  }
  return [super canPerformAction:action withSender:sender];
}

- (void)validateCommand:(UICommand*)command {
  if (command.action == @selector(keyCommand_find)) {
    command.discoverabilityTitle =
        l10n_util::GetNSStringWithFixup(IDS_IOS_KEYBOARD_SEARCH_TABS);
  } else {
    // TODO(crbug.com/40246790): Add string for change pane's functions.
    return [super validateCommand:command];
  }
}

- (void)keyCommand_openNewTab {
  base::RecordAction(base::UserMetricsAction("MobileKeyCommandOpenNewTab"));
  [self openNewTabInCurrentPageForKeyboardCommand];
}

- (void)keyCommand_openNewRegularTab {
  base::RecordAction(
      base::UserMetricsAction("MobileKeyCommandOpenNewRegularTab"));
  [self openNewRegularTabForKeyboardCommand];
}

- (void)keyCommand_openNewIncognitoTab {
  base::RecordAction(
      base::UserMetricsAction("MobileKeyCommandOpenNewIncognitoTab"));
  [self openNewIncognitoTabForKeyboardCommand];
}

- (void)keyCommand_select1 {
  base::RecordAction(
      base::UserMetricsAction("MobileKeyCommandGoToIncognitoTabGrid"));
  [self setCurrentPageAndPageControl:TabGridPageIncognitoTabs animated:YES];
}

- (void)keyCommand_select2 {
  base::RecordAction(
      base::UserMetricsAction("MobileKeyCommandGoToRegularTabGrid"));
  [self setCurrentPageAndPageControl:TabGridPageRegularTabs animated:YES];
}

- (void)keyCommand_select3 {
  base::RecordAction(
      base::UserMetricsAction("MobileKeyCommandGoToRemoteTabGrid"));
  [self setCurrentPageAndPageControl:TabGridPageRemoteTabs animated:YES];
}

// Returns `YES` if should use compact layout.
- (BOOL)shouldUseCompactLayout {
  return self.traitCollection.verticalSizeClass ==
             UIUserInterfaceSizeClassRegular &&
         self.traitCollection.horizontalSizeClass ==
             UIUserInterfaceSizeClassCompact;
}

// Updates and sets constraints for `pinnedTabsViewController`.
- (void)updatePinnedTabsViewControllerConstraints {
  if ([self.pinnedTabsConstraints count] > 0) {
    [NSLayoutConstraint deactivateConstraints:self.pinnedTabsConstraints];
    self.pinnedTabsConstraints = nil;
  }

  UIView* pinnedView = self.pinnedTabsViewController.view;
  NSMutableArray<NSLayoutConstraint*>* pinnedTabsConstraints =
      [[NSMutableArray alloc] init];
  BOOL compactLayout = [self shouldUseCompactLayout];

  if (compactLayout) {
    [pinnedTabsConstraints addObjectsFromArray:@[
      [pinnedView.leadingAnchor
          constraintEqualToAnchor:self.view.leadingAnchor
                         constant:kPinnedViewHorizontalPadding],
      [pinnedView.trailingAnchor
          constraintEqualToAnchor:self.view.trailingAnchor
                         constant:-kPinnedViewHorizontalPadding],
      [pinnedView.bottomAnchor
          constraintEqualToAnchor:self.bottomToolbar.topAnchor
                         constant:-kPinnedViewBottomPadding],
    ]];
  } else {
    [pinnedTabsConstraints addObjectsFromArray:@[
      [pinnedView.centerXAnchor
          constraintEqualToAnchor:self.view.centerXAnchor],
      [pinnedView.widthAnchor
          constraintEqualToAnchor:self.view.widthAnchor
                       multiplier:kPinnedViewMaxWidthInPercent],
      [pinnedView.topAnchor
          constraintEqualToAnchor:self.bottomToolbar.topAnchor],
    ]];
  }

  self.pinnedTabsConstraints = pinnedTabsConstraints;
  [NSLayoutConstraint activateConstraints:self.pinnedTabsConstraints];
}

#pragma mark - GridConsumer

- (void)setActivePageFromPage:(TabGridPage)page {
  self.activePage = page;
}

- (void)prepareForDismissal {
  [self.incognitoTabsViewController prepareForDismissal];
  [self.regularTabsViewController prepareForDismissal];
}

#pragma mark - GestureInProductHelpViewDelegate

- (void)gestureInProductHelpView:(GestureInProductHelpView*)view
            didDismissWithReason:(IPHDismissalReasonType)reason {
  [self.delegate tabGridDidDismissSwipeToIncognitoIPHWithReason:reason];
}

- (void)gestureInProductHelpView:(GestureInProductHelpView*)view
    shouldHandleSwipeInDirection:(UISwipeGestureRecognizerDirection)direction {
  [self.mutator pageChanged:TabGridPageIncognitoTabs
                interaction:TabSwitcherPageChangeInteraction::kScrollDrag];
  [self setCurrentPageAndPageControl:TabGridPageIncognitoTabs animated:YES];
}

#pragma mark - TabGridIdleStatusHandler

- (BOOL)status {
  return _idleTabGrid && !_pageChangedSinceEntering &&
         !_backgroundedSinceEntering;
}

- (void)tabGridDidPerformAction:(TabGridActionType)type {
  if (self.viewVisible) {
    switch (type) {
      case TabGridActionType::kInPageAction:
        _idleTabGrid = NO;
        break;
      case TabGridActionType::kChangePage:
        _pageChangedSinceEntering = YES;
        break;
      case TabGridActionType::kBackground:
        _backgroundedSinceEntering = YES;
        break;
    }
  }
}

@end