chromium/ios/chrome/browser/ui/omnibox/popup/omnibox_popup_view_controller.mm

// Copyright 2019 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/omnibox/popup/omnibox_popup_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/format_macros.h"
#import "base/logging.h"
#import "base/metrics/histogram_macros.h"
#import "base/time/time.h"
#import "components/favicon/core/large_icon_service.h"
#import "components/omnibox/common/omnibox_features.h"
#import "ios/chrome/browser/favicon/ui_bundled/favicon_attributes_provider.h"
#import "ios/chrome/browser/favicon/ui_bundled/favicon_attributes_with_payload.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/shared/ui/elements/self_sizing_table_view.h"
#import "ios/chrome/browser/shared/ui/util/keyboard_observer_helper.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/util_swift.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_tile_layout_util.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_constants.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_ui_features.h"
#import "ios/chrome/browser/ui/omnibox/popup/autocomplete_suggestion.h"
#import "ios/chrome/browser/ui/omnibox/popup/carousel/carousel_item.h"
#import "ios/chrome/browser/ui/omnibox/popup/carousel/omnibox_popup_carousel_cell.h"
#import "ios/chrome/browser/ui/omnibox/popup/content_providing.h"
#import "ios/chrome/browser/ui/omnibox/popup/omnibox_popup_accessibility_identifier_constants.h"
#import "ios/chrome/browser/ui/omnibox/popup/popup_match_preview_delegate.h"
#import "ios/chrome/browser/ui/omnibox/popup/row/actions/omnibox_popup_actions_row_content_configuration.h"
#import "ios/chrome/browser/ui/omnibox/popup/row/actions/omnibox_popup_actions_row_delegate.h"
#import "ios/chrome/browser/ui/omnibox/popup/row/actions/suggest_action.h"
#import "ios/chrome/browser/ui/omnibox/popup/row/omnibox_popup_row_content_configuration.h"
#import "ios/chrome/browser/ui/omnibox/popup/row/omnibox_popup_row_delegate.h"
#import "ios/chrome/browser/ui/toolbar/buttons/toolbar_configuration.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/device_util.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ui/base/device_form_factor.h"

namespace {
const CGFloat kTopPadding = 8.0;
const CGFloat kBottomPadding = 8.0;
const CGFloat kFooterHeight = 4.0;
/// Percentage of the suggestion height that needs to be visible in order to
/// consider the suggestion as visible.
const CGFloat kVisibleSuggestionThreshold = 0.6;
/// Minimum size of the fetched favicon for tiles.
const CGFloat kMinTileFaviconSize = 32.0f;
/// Maximum size of the fetched favicon for tiles.
const CGFloat kMaxTileFaviconSize = 48.0f;

/// Bottom padding for table view headers.
const CGFloat kHeaderPaddingBottom = 10.0f;
/// Leading and trailing padding for table view headers.
const CGFloat kHeaderPadding = 2.0f;
/// Top padding for table view headers.
const CGFloat kHeaderTopPadding = 16.0f;

}  // namespace

@interface OmniboxPopupViewController () <OmniboxPopupActionsRowDelegate,
                                          OmniboxPopupCarouselCellDelegate,
                                          OmniboxPopupRowDelegate,
                                          UITableViewDataSource,
                                          UITableViewDelegate>

/// Index path of currently highlighted row. The rows can be highlighted by
/// tapping and holding on them or by using arrow keys on a hardware keyboard.
@property(nonatomic, strong) NSIndexPath* highlightedIndexPath;

/// Flag that enables forwarding scroll events to the delegate. Disabled while
/// updating the cells to avoid defocusing the omnibox when the omnibox popup
/// changes size and table view issues a scroll event.
@property(nonatomic, assign) BOOL forwardsScrollEvents;

/// The height of the keyboard. Used to determine the content inset for the
/// scroll view.
@property(nonatomic, assign) CGFloat keyboardHeight;

/// Time the view appeared on screen. Used to record a metric of how long this
/// view controller was on screen.
@property(nonatomic, assign) base::TimeTicks viewAppearanceTime;
/// Table view that displays the results.
@property(nonatomic, strong) UITableView* tableView;

/// Alignment of omnibox text. Popup text should match this alignment.
@property(nonatomic, assign) NSTextAlignment alignment;

/// Semantic content attribute of omnibox text. Popup should match this
/// attribute. This is used by the new omnibox popup.
@property(nonatomic, assign)
    UISemanticContentAttribute semanticContentAttribute;

/// Estimated maximum number of visible suggestions.
/// Only updated in `newResultsAvailable` method, were the value is used.
@property(nonatomic, assign) NSUInteger visibleSuggestionCount;

/// Boolean to update visible suggestion count only once on event such as device
/// orientation change or multitasking window change, where multiple keyboard
/// and view updates are received.
@property(nonatomic, assign) BOOL shouldUpdateVisibleSuggestionCount;

/// Index of the suggestion group that contains the first suggestion to preview
/// and highlight.
@property(nonatomic, assign) NSUInteger preselectedMatchGroupIndex;

/// Provider used to fetch carousel favicons.
@property(nonatomic, strong)
    FaviconAttributesProvider* carouselAttributeProvider;

/// UITableViewCell displaying the most visited carousel in (Web and SRP) ZPS
/// state.
@property(nonatomic, strong) OmniboxPopupCarouselCell* carouselCell;

/// Flag that tracks if the carousel should be hidden. It is only true when we
/// show the carousel, then the user deletes every item in it before the UI has
/// updated.
@property(nonatomic, assign) BOOL shouldHideCarousel;

/// Cached `tableView.visibleContentSize.height` used in `viewDidLayoutSubviews`
/// to avoid infinite loop and redudant computation when updating table view's
/// content inset.
@property(nonatomic, assign) CGFloat cachedContentHeight;

/// Layout guide that tracks the position of the omnibox in the top toolbar.
/// This is useful to add constraints to, or to derive manual layout values off
/// of.
@property(nonatomic, readonly) UILayoutGuide* omniboxGuide;

@end

@implementation OmniboxPopupViewController

@synthesize omniboxGuide = _omniboxGuide;

- (instancetype)init {
  if ((self = [super initWithNibName:nil bundle:nil])) {
    _forwardsScrollEvents = YES;
    _preselectedMatchGroupIndex = 0;
    _visibleSuggestionCount = 0;
    _cachedContentHeight = 0;
    NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
    // Listen to keyboard frame change event to detect keyboard frame changes
    // (ex: when changing input method) to update the estimated number of
    // visible suggestions.
    [defaultCenter addObserver:self
                      selector:@selector(keyboardDidChangeFrame:)
                          name:UIKeyboardDidChangeFrameNotification
                        object:nil];

    // Listen to content size change to update the estimated number of visible
    // suggestions.
    [defaultCenter addObserver:self
                      selector:@selector(contentSizeDidChange:)
                          name:UIContentSizeCategoryDidChangeNotification
                        object:nil];
  }
  return self;
}

- (void)loadView {
  // TODO(crbug.com/40866206): Check why largeIconService not available in
  // incognito.
  if (self.largeIconService) {
    _carouselAttributeProvider = [[FaviconAttributesProvider alloc]
        initWithFaviconSize:kMaxTileFaviconSize
             minFaviconSize:kMinTileFaviconSize
           largeIconService:self.largeIconService];
    _carouselAttributeProvider.cache = self.largeIconCache;
  }
  self.tableView =
      [[SelfSizingTableView alloc] initWithFrame:CGRectZero
                                           style:UITableViewStyleGrouped];
  self.tableView.delegate = self;
  self.tableView.dataSource = self;
  self.view = self.tableView;
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  [self updateBackgroundColor];
  if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    [self.delegate autocompleteResultConsumerDidChangeTraitCollection:self];
  }
}

- (void)toggleOmniboxDebuggerView {
  if (self.debugInfoViewController.viewIfLoaded.window) {
    [self dismissViewControllerAnimated:YES completion:nil];
  } else {
    [self showDebugUI];
  }
}

#pragma mark - Getter/Setter

- (void)setHighlightedIndexPath:(NSIndexPath*)highlightedIndexPath {
  // Special case for highlight moving inside a carousel-style section.
  if (_highlightedIndexPath &&
      highlightedIndexPath.section == _highlightedIndexPath.section &&
      self.currentResult[highlightedIndexPath.section].displayStyle ==
          SuggestionGroupDisplayStyleCarousel) {
    // The highlight moved inside the section horizontally. No need to
    // unhighlight the previous row. Just notify the delegate.
    _highlightedIndexPath = highlightedIndexPath;
    [self didHighlightSelectedSuggestion];
    return;
  }

  // General case: highlighting moved between different rows.
  if (_highlightedIndexPath) {
    [self unhighlightRowAtIndexPath:_highlightedIndexPath];
  }
  _highlightedIndexPath = highlightedIndexPath;
  if (highlightedIndexPath) {
    [self highlightRowAtIndexPath:_highlightedIndexPath];
    [self didHighlightSelectedSuggestion];
  }
}

- (OmniboxPopupCarouselCell*)carouselCell {
  if (!_carouselCell) {
    _carouselCell = [[OmniboxPopupCarouselCell alloc] init];
    _carouselCell.delegate = self;
    _carouselCell.menuProvider = self.carouselMenuProvider;
  }
  return _carouselCell;
}

#pragma mark - View lifecycle

- (void)viewDidLoad {
  [super viewDidLoad];

  self.tableView.accessibilityIdentifier =
      kOmniboxPopupTableViewAccessibilityIdentifier;
  self.tableView.insetsContentViewsToSafeArea = YES;

  // Initialize the same size as the parent view, autoresize will correct this.
  [self.view setFrame:CGRectZero];
  [self.view setAutoresizingMask:(UIViewAutoresizingFlexibleWidth |
                                  UIViewAutoresizingFlexibleHeight)];

  [self updateBackgroundColor];

  // Table configuration.
  self.tableView.allowsMultipleSelectionDuringEditing = NO;
  self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
  self.tableView.separatorInset = UIEdgeInsetsZero;
  if ([self.tableView respondsToSelector:@selector(setLayoutMargins:)]) {
    [self.tableView setLayoutMargins:UIEdgeInsetsZero];
  }
  self.tableView.contentInsetAdjustmentBehavior =
      UIScrollViewContentInsetAdjustmentAutomatic;

  if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    self.tableView.tableFooterView =
        [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, FLT_MIN)];
    [self.tableView
        setDirectionalLayoutMargins:NSDirectionalEdgeInsetsMake(
                                        kTopPadding, 0, kBottomPadding, 0)];
    self.tableView.contentInset =
        UIEdgeInsetsMake(kTopPadding, 0, kBottomPadding, 0);
  } else {
    [self.tableView setDirectionalLayoutMargins:NSDirectionalEdgeInsetsMake(
                                                    0, 0, kBottomPadding, 0)];
    self.tableView.contentInset = UIEdgeInsetsMake(kTopPadding, 0, 0, 0);
  }

  self.tableView.sectionHeaderHeight = 0.1;
  self.tableView.estimatedRowHeight = 0;

  self.tableView.rowHeight = UITableViewAutomaticDimension;
  self.tableView.estimatedRowHeight = kOmniboxPopupCellMinimumHeight;

  [self.tableView registerClass:[UITableViewCell class]
         forCellReuseIdentifier:OmniboxPopupRowCellReuseIdentifier];
  [self.tableView registerClass:[UITableViewCell class]
         forCellReuseIdentifier:OmniboxPopupActionsRowCellReuseIdentifier];
  [self.tableView registerClass:[UITableViewHeaderFooterView class]
      forHeaderFooterViewReuseIdentifier:NSStringFromClass(
                                             [UITableViewHeaderFooterView
                                                 class])];
  self.shouldUpdateVisibleSuggestionCount = YES;
  self.tableView.sectionHeaderTopPadding = 0;
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  [self adjustMarginsToMatchOmniboxWidth];

  self.viewAppearanceTime = base::TimeTicks::Now();
}

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];
  UMA_HISTOGRAM_MEDIUM_TIMES("MobileOmnibox.PopupOpenDuration",
                             base::TimeTicks::Now() - self.viewAppearanceTime);
}

- (void)viewWillTransitionToSize:(CGSize)size
       withTransitionCoordinator:
           (id<UIViewControllerTransitionCoordinator>)coordinator {
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  [self.tableView setEditing:NO animated:NO];
  self.shouldUpdateVisibleSuggestionCount = YES;

  __weak __typeof__(self) weakSelf = self;

  [coordinator
      animateAlongsideTransition:^(
          id<UIViewControllerTransitionCoordinatorContext> context) {
        [weakSelf adjustMarginsToMatchOmniboxWidth];
      }
      completion:^(id<UIViewControllerTransitionCoordinatorContext>) {
        // Make sure the margins are correct after the animation.
        [weakSelf adjustMarginsToMatchOmniboxWidth];
      }];
}

- (void)adjustMarginsToMatchOmniboxWidth {
  if (!self.omniboxGuide) {
    return;
  }

  // Adjust the carousel to be aligned with the omnibox textfield.
  UIEdgeInsets margins = self.carouselCell.layoutMargins;
  self.carouselCell.layoutMargins =
      UIEdgeInsetsMake(margins.top, 0, margins.bottom, 0);

  // Update the headers padding.
  for (NSInteger i = 0; i < self.tableView.numberOfSections; ++i) {
    UITableViewHeaderFooterView* headerView =
        [self.tableView headerViewForSection:i];
    [headerView setNeedsUpdateConfiguration];
  }

  // Update cells' configuration to realign the text to the omnibox.
  for (UITableViewCell* cell in self.tableView.visibleCells) {
    if ([cell.contentConfiguration
            isKindOfClass:OmniboxPopupRowContentConfiguration.class]) {
      [cell setNeedsUpdateConfiguration];
    }
  }
}

#pragma mark - AutocompleteResultConsumer

- (void)updateMatches:(NSArray<id<AutocompleteSuggestionGroup>>*)result
    preselectedMatchGroupIndex:(NSInteger)groupIndex {
  DCHECK(groupIndex == 0 || groupIndex < (NSInteger)result.count);
  self.shouldHideCarousel = NO;
  self.forwardsScrollEvents = NO;
  // Reset highlight state.
  self.highlightedIndexPath = nil;

  self.preselectedMatchGroupIndex = groupIndex;
  self.currentResult = result;

  [self.tableView reloadData];
  self.forwardsScrollEvents = YES;
  id<AutocompleteSuggestion> firstSuggestionOfPreselectedGroup =
      [self suggestionAtIndexPath:[NSIndexPath indexPathForRow:0
                                                     inSection:groupIndex]];
  [self.matchPreviewDelegate
      setPreviewSuggestion:firstSuggestionOfPreselectedGroup
             isFirstUpdate:YES];
}

/// Set text alignment for popup cells.
- (void)setTextAlignment:(NSTextAlignment)alignment {
  self.alignment = alignment;
}

- (void)setDebugInfoViewController:(UIViewController*)viewController {
  DCHECK(experimental_flags::IsOmniboxDebuggingEnabled());
  _debugInfoViewController = viewController;

  UITapGestureRecognizer* debugGestureRecognizer =
      [[UITapGestureRecognizer alloc] initWithTarget:self
                                              action:@selector(showDebugUI)];
#if TARGET_OS_SIMULATOR
  // One tap for easy trigger on simulator.
  debugGestureRecognizer.numberOfTapsRequired = 1;
#else
  debugGestureRecognizer.numberOfTapsRequired = 2;
#endif
  debugGestureRecognizer.numberOfTouchesRequired = 2;
  [self.view addGestureRecognizer:debugGestureRecognizer];
}

- (void)newResultsAvailable {
  if (self.shouldUpdateVisibleSuggestionCount) {
    [self updateVisibleSuggestionCount];
  }
  [self.dataSource
      requestResultsWithVisibleSuggestionCount:self.visibleSuggestionCount];
}

#pragma mark - OmniboxKeyboardDelegate

- (BOOL)canPerformKeyboardAction:(OmniboxKeyboardAction)keyboardAction {
  UITableViewCell* cell =
      [self.tableView cellForRowAtIndexPath:self.highlightedIndexPath];

  BOOL isActionsRowCell = [cell.contentConfiguration
      isKindOfClass:OmniboxPopupActionsRowContentConfiguration.class];

  if (isActionsRowCell) {
    OmniboxPopupActionsRowContentConfiguration* configuration =
        base::apple::ObjCCastStrict<OmniboxPopupActionsRowContentConfiguration>(
            cell.contentConfiguration);
    if ([configuration canPerformKeyboardAction:keyboardAction]) {
      return YES;
    }
  }

  switch (keyboardAction) {
    case OmniboxKeyboardActionUpArrow:
    case OmniboxKeyboardActionDownArrow:
      return YES;
    case OmniboxKeyboardActionLeftArrow:
    case OmniboxKeyboardActionRightArrow:
      if (self.carouselCell.isHighlighted) {
        return [self.carouselCell canPerformKeyboardAction:keyboardAction];
      }
      return NO;
  }
}

- (void)performKeyboardAction:(OmniboxKeyboardAction)keyboardAction {
  DCHECK([self canPerformKeyboardAction:keyboardAction]);

  UITableViewCell* cell =
      [self.tableView cellForRowAtIndexPath:self.highlightedIndexPath];

  BOOL isActionsRowCell = [cell.contentConfiguration
      isKindOfClass:OmniboxPopupActionsRowContentConfiguration.class];

  if (isActionsRowCell) {
    OmniboxPopupActionsRowContentConfiguration* configuration =
        base::apple::ObjCCastStrict<OmniboxPopupActionsRowContentConfiguration>(
            cell.contentConfiguration);
    if ([configuration canPerformKeyboardAction:keyboardAction]) {
      [configuration performKeyboardAction:keyboardAction];
      cell.contentConfiguration = configuration;
      return;
    }
  }

  switch (keyboardAction) {
    case OmniboxKeyboardActionUpArrow:
      [self highlightPreviousSuggestion];
      break;
    case OmniboxKeyboardActionDownArrow:
      [self highlightNextSuggestion];
      break;
    case OmniboxKeyboardActionLeftArrow:
    case OmniboxKeyboardActionRightArrow:
      if (self.carouselCell.isHighlighted) {
        DCHECK(self.highlightedIndexPath.section ==
               [self.tableView indexPathForCell:self.carouselCell].section);

        [self.carouselCell performKeyboardAction:keyboardAction];
        NSInteger highlightedTileIndex = self.carouselCell.highlightedTileIndex;
        if (highlightedTileIndex == NSNotFound) {
          self.highlightedIndexPath = nil;
        } else {
          self.highlightedIndexPath =
              [NSIndexPath indexPathForRow:highlightedTileIndex
                                 inSection:self.highlightedIndexPath.section];
        }
      }
      break;
  }
}

#pragma mark OmniboxKeyboardDelegate Private

- (void)highlightPreviousSuggestion {
  NSIndexPath* path = self.highlightedIndexPath;
  if (path == nil) {
    // If there is a section above `preselectedMatchGroupIndex` select the last
    // suggestion of this section.
    if (self.preselectedMatchGroupIndex > 0 &&
        self.currentResult.count > self.preselectedMatchGroupIndex - 1) {
      NSInteger sectionAbovePreselectedGroup =
          self.preselectedMatchGroupIndex - 1;
      NSIndexPath* suggestionIndex = [NSIndexPath
          indexPathForRow:(NSInteger)self
                              .currentResult[sectionAbovePreselectedGroup]
                              .suggestions.count -
                          1
                inSection:sectionAbovePreselectedGroup];
      if ([self suggestionAtIndexPath:suggestionIndex]) {
        self.highlightedIndexPath = suggestionIndex;
      }
    }
    return;
  }

  id<AutocompleteSuggestionGroup> suggestionGroup =
      self.currentResult[self.highlightedIndexPath.section];
  BOOL isCurrentHighlightedRowFirstInSection =
      suggestionGroup.displayStyle == SuggestionGroupDisplayStyleCarousel ||
      (path.row == 0);

  if (isCurrentHighlightedRowFirstInSection) {
    NSInteger previousSection = path.section - 1;
    NSInteger previousSectionCount =
        (previousSection >= 0)
            ? [self.tableView numberOfRowsInSection:previousSection]
            : 0;
    BOOL prevSectionHasItems = previousSectionCount > 0;
    if (prevSectionHasItems) {
      path = [NSIndexPath indexPathForRow:previousSectionCount - 1
                                inSection:previousSection];
    } else {
      // Can't move up from first row. Call the delegate again so that the
      // inline autocomplete text is set again (in case the user exited the
      // inline autocomplete).
      [self didHighlightSelectedSuggestion];
      return;
    }
  } else {
    path = [NSIndexPath indexPathForRow:path.row - 1 inSection:path.section];
  }

  [self.tableView scrollToRowAtIndexPath:path
                        atScrollPosition:UITableViewScrollPositionTop
                                animated:NO];

  self.highlightedIndexPath = path;
}

- (void)highlightNextSuggestion {
  if (!self.highlightedIndexPath) {
    NSIndexPath* preselectedSuggestionIndex =
        [NSIndexPath indexPathForRow:0
                           inSection:self.preselectedMatchGroupIndex];
    if ([self suggestionAtIndexPath:preselectedSuggestionIndex]) {
      self.highlightedIndexPath = preselectedSuggestionIndex;
    }
    return;
  }

  NSIndexPath* path = self.highlightedIndexPath;
  id<AutocompleteSuggestionGroup> suggestionGroup =
      self.currentResult[self.highlightedIndexPath.section];
  BOOL isCurrentHighlightedRowLastInSection =
      suggestionGroup.displayStyle == SuggestionGroupDisplayStyleCarousel ||
      path.row == [self.tableView numberOfRowsInSection:path.section] - 1;
  if (isCurrentHighlightedRowLastInSection) {
    NSInteger nextSection = path.section + 1;
    BOOL nextSectionHasItems =
        [self.tableView numberOfSections] > nextSection &&
        [self.tableView numberOfRowsInSection:nextSection] > 0;

    if (nextSectionHasItems) {
      path = [NSIndexPath indexPathForRow:0 inSection:nextSection];
    } else {
      // Can't go below last row. Call the delegate again so that the inline
      // autocomplete text is set again (in case the user exited the inline
      // autocomplete).
      [self didHighlightSelectedSuggestion];
      return;
    }
  } else {
    path = [NSIndexPath indexPathForRow:path.row + 1 inSection:path.section];
  }

  [self.tableView scrollToRowAtIndexPath:path
                        atScrollPosition:UITableViewScrollPositionBottom
                                animated:NO];

  // There is a row below, move highlight there.
  self.highlightedIndexPath = path;
}

- (void)highlightRowAtIndexPath:(NSIndexPath*)indexPath {
  if (self.currentResult[indexPath.section].displayStyle ==
      SuggestionGroupDisplayStyleCarousel) {
    indexPath = [NSIndexPath indexPathForRow:0 inSection:indexPath.section];
  }
  UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
  [cell setHighlighted:YES animated:NO];
}

- (void)unhighlightRowAtIndexPath:(NSIndexPath*)indexPath {
  if (self.currentResult[indexPath.section].displayStyle ==
      SuggestionGroupDisplayStyleCarousel) {
    indexPath = [NSIndexPath indexPathForRow:0 inSection:indexPath.section];
  }
  UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
  [cell setHighlighted:NO animated:NO];
}

- (void)didHighlightSelectedSuggestion {
  id<AutocompleteSuggestion> suggestion =
      [self suggestionAtIndexPath:self.highlightedIndexPath];
  DCHECK(suggestion);
  [self.matchPreviewDelegate setPreviewSuggestion:suggestion isFirstUpdate:NO];
}

#pragma mark - OmniboxPopupRowDelegate

- (void)omniboxPopupRowWithConfiguration:
            (OmniboxPopupRowContentConfiguration*)configuration
         didTapTrailingButtonAtIndexPath:(NSIndexPath*)indexPath {
  id<AutocompleteSuggestion> suggestion =
      [self suggestionAtIndexPath:indexPath];
  if (suggestion != configuration.suggestion) {
    return;
  }
  [self.delegate autocompleteResultConsumer:self
           didTapTrailingButtonOnSuggestion:suggestion
                                      inRow:indexPath.row];
}

- (void)omniboxPopupRowWithConfiguration:
            (OmniboxPopupRowContentConfiguration*)configuration
    didUpdateAccessibilityActionsAtIndexPath:(NSIndexPath*)indexPath {
  id<AutocompleteSuggestion> suggestion =
      [self suggestionAtIndexPath:indexPath];
  if (suggestion != configuration.suggestion) {
    return;
  }
  // Actions reference the configuration that created them. When applying a
  // new configuration to the content view, also update the actions to avoid
  // retaining the old configuration.
  UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
  cell.accessibilityCustomActions = configuration.accessibilityCustomActions;
}

#pragma mark - OmniboxPopupActionsRowDelegate

- (void)omniboxPopupRowActionSelectedWithConfiguration:
            (OmniboxPopupActionsRowContentConfiguration*)configuration
                                                action:(SuggestAction*)action {
  id<AutocompleteSuggestion> suggestion =
      [self suggestionAtIndexPath:configuration.indexPath];

  CHECK(suggestion == configuration.suggestion);

  [self.delegate autocompleteResultConsumer:self
                  didSelectSuggestionAction:action
                                 suggestion:suggestion
                                      inRow:configuration.indexPath.row];
}

#pragma mark - OmniboxReturnDelegate

- (void)omniboxReturnPressed:(id)sender {
  if (self.highlightedIndexPath) {
    id<AutocompleteSuggestion> suggestion =
        [self suggestionAtIndexPath:self.highlightedIndexPath];

    UITableViewCell* cell =
        [self.tableView cellForRowAtIndexPath:self.highlightedIndexPath];
    BOOL isActionsRowCell = [cell.contentConfiguration
        isKindOfClass:OmniboxPopupActionsRowContentConfiguration.class];

    if (isActionsRowCell) {
      OmniboxPopupActionsRowContentConfiguration* config =
          base::apple::ObjCCastStrict<
              OmniboxPopupActionsRowContentConfiguration>(
              cell.contentConfiguration);
      if (config.highlightedActionIndex != NSNotFound) {
        [config omniboxReturnPressed:sender];
        return;
      }
    }

    if (suggestion) {
      NSInteger absoluteRow =
          [self absoluteRowIndexForIndexPath:self.highlightedIndexPath];
      [self.delegate autocompleteResultConsumer:self
                            didSelectSuggestion:suggestion
                                          inRow:absoluteRow];
      return;
    }
  }
  [self.acceptReturnDelegate omniboxReturnPressed:sender];
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
      willDisplayCell:(UITableViewCell*)cell
    forRowAtIndexPath:(NSIndexPath*)indexPath {
  if ([cell.contentConfiguration
          isKindOfClass:OmniboxPopupRowContentConfiguration.class]) {
    cell.accessibilityIdentifier = [OmniboxPopupAccessibilityIdentifierHelper
        accessibilityIdentifierForRowAtIndexPath:indexPath];
  }
}

- (BOOL)tableView:(UITableView*)tableView
    shouldHighlightRowAtIndexPath:(NSIndexPath*)indexPath {
  return YES;
}

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  NSUInteger row = indexPath.row;
  NSUInteger section = indexPath.section;

  // In rare cases when the device is slow, user might be able to tap a
  // suggestion row twice before the event is being delivered. In this case, on
  // the second touch, the popup will already be cleared, but the table view
  // will still dispatch a didSelectRowAtIndexPath event for a non-existent
  // index path. Ignore these double touches.
  if (section >= self.currentResult.count ||
      row >= self.currentResult[indexPath.section].suggestions.count)
    return;
  NSInteger absoluteRow = [self absoluteRowIndexForIndexPath:indexPath];
  [self.delegate
      autocompleteResultConsumer:self
             didSelectSuggestion:[self suggestionAtIndexPath:indexPath]
                           inRow:absoluteRow];
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForHeaderInSection:(NSInteger)section {
  BOOL hasTitle = self.currentResult[section].title.length > 0;
  return hasTitle ? UITableViewAutomaticDimension : FLT_MIN;
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForFooterInSection:(NSInteger)section {
  // Don't show the footer on the last section, to not increase the size of the
  // popup on iPad.
  if (section == (tableView.numberOfSections - 1)) {
    return FLT_MIN;
  }

  // When most visited tiles are enabled, only allow section separator under the
  // verbatim suggestion.
  if (section > 0) {
    return FLT_MIN;
  }

  return kFooterHeight;
}

- (UIView*)tableView:(UITableView*)tableView
    viewForFooterInSection:(NSInteger)section {
  // Do not show footer for the last section
  if (section == (tableView.numberOfSections - 1)) {
    return nil;
  }
  // Do not show footer when there is a header for the next section.
  if (self.currentResult[section + 1].title.length > 0) {
    return nil;
  }

  UIView* footer = [[UIView alloc] init];
  footer.backgroundColor = tableView.backgroundColor;
  UIView* hairline = [[UIView alloc]
      initWithFrame:CGRectMake(0, 0, tableView.bounds.size.width,
                               2 / tableView.window.screen.scale)];

  hairline.backgroundColor = [UIColor
      colorNamed:ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET
                     ? kOmniboxPopoutSuggestionRowSeparatorColor
                     : kOmniboxSuggestionRowSeparatorColor];
  [footer addSubview:hairline];
  hairline.autoresizingMask = UIViewAutoresizingFlexibleWidth;

  return footer;
}

#pragma mark - UITableViewDataSource

- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView {
  return self.currentResult.count;
}

- (NSInteger)tableView:(UITableView*)tableView
    numberOfRowsInSection:(NSInteger)section {
  switch (self.currentResult[section].displayStyle) {
    case SuggestionGroupDisplayStyleDefault:
      return self.currentResult[section].suggestions.count;
    case SuggestionGroupDisplayStyleCarousel:
      if (self.shouldHideCarousel) {
        return 0;
      }

      // The carousel displays suggestions on one row.
      return 1;
  }
}

- (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath {
  // iOS doesn't check -numberOfRowsInSection before checking
  // -canEditRowAtIndexPath in a reload call. If `indexPath.row` is too large,
  // simple return `NO`.
  if ((NSUInteger)indexPath.row >=
      self.currentResult[indexPath.section].suggestions.count)
    return NO;

  if (self.currentResult[indexPath.section].displayStyle ==
      SuggestionGroupDisplayStyleCarousel) {
    return NO;
  }

  return [self.currentResult[indexPath.section].suggestions[indexPath.row]
      supportsDeletion];
}

- (void)tableView:(UITableView*)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
     forRowAtIndexPath:(NSIndexPath*)indexPath {
  DCHECK_LT((NSUInteger)indexPath.row,
            self.currentResult[indexPath.section].suggestions.count);
  id<AutocompleteSuggestion> suggestion =
      [self suggestionAtIndexPath:indexPath];
  DCHECK(suggestion);
  if (editingStyle == UITableViewCellEditingStyleDelete) {
    [self.delegate autocompleteResultConsumer:self
               didSelectSuggestionForDeletion:suggestion
                                        inRow:indexPath.row];
  }
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForRowAtIndexPath:(NSIndexPath*)indexPath {
  return UITableViewAutomaticDimension;
}

- (UIView*)tableView:(UITableView*)tableView
    viewForHeaderInSection:(NSInteger)section {
  NSString* title = self.currentResult[section].title;
  if (!title) {
    return nil;
  }

  UITableViewHeaderFooterView* header =
      [tableView dequeueReusableHeaderFooterViewWithIdentifier:
                     NSStringFromClass([UITableViewHeaderFooterView class])];

  UIListContentConfiguration* contentConfiguration =
      header.defaultContentConfiguration;

  contentConfiguration.text = title;
  contentConfiguration.textProperties.font =
      [UIFont systemFontOfSize:14 weight:UIFontWeightSemibold];
  contentConfiguration.textProperties.transform =
      UIListContentTextTransformUppercase;
  contentConfiguration.directionalLayoutMargins = NSDirectionalEdgeInsetsMake(
      kHeaderTopPadding, kHeaderPadding, kHeaderPaddingBottom, kHeaderPadding);

  __weak __typeof__(self) weakSelf = self;
  UITableViewHeaderFooterViewConfigurationUpdateHandler configurationUpdater =
      ^void(__kindof UITableViewHeaderFooterView* headerView,
            UIViewConfigurationState* state) {
        __typeof__(self) strongSelf = weakSelf;
        if (!strongSelf) {
          return;
        }
        // Inset the header to match the omnibox width, similar to
        // `adjustMarginsToMatchOmniboxWidth` method.
        CGFloat leadingPadding = kHeaderPadding;
        if (IsRegularXRegularSizeClass(strongSelf) && strongSelf.omniboxGuide) {
          leadingPadding += CGRectGetMinX(weakSelf.omniboxGuide.layoutFrame);
        }

        UIListContentConfiguration* configurationCopy =
            (UIListContentConfiguration*)headerView.contentConfiguration;
        configurationCopy.directionalLayoutMargins =
            NSDirectionalEdgeInsetsMake(kHeaderTopPadding, leadingPadding,
                                        kHeaderPaddingBottom, kHeaderPadding);
        headerView.contentConfiguration = configurationCopy;
      };
  header.contentConfiguration = contentConfiguration;
  header.configurationUpdateHandler = configurationUpdater;
  return header;
}

- (void)tableView:(UITableView*)tableView
    didEndDisplayingCell:(UITableViewCell*)cell
       forRowAtIndexPath:(NSIndexPath*)indexPath {
  // Action in suggest buttons respond to touch-up events which could be
  // triggered after the cell was removed (see b/350911243).
  // Remove the delegate from a cell when it is no longer visible, ensuring that
  // actions are not dispatched for stale cells.
  BOOL isActionsRowCell = [cell.contentConfiguration
      isKindOfClass:OmniboxPopupActionsRowContentConfiguration.class];

  if (!isActionsRowCell) {
    return;
  }

  OmniboxPopupActionsRowContentConfiguration* configuration =
      base::apple::ObjCCastStrict<OmniboxPopupActionsRowContentConfiguration>(
          cell.contentConfiguration);

  configuration.delegate = nil;
}

/// Customize the appearance of table view cells.
- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  DCHECK_LT((NSUInteger)indexPath.row,
            self.currentResult[indexPath.section].suggestions.count);

  switch (self.currentResult[indexPath.section].displayStyle) {
    case SuggestionGroupDisplayStyleDefault: {
      id<AutocompleteSuggestion> suggestion =
          self.currentResult[indexPath.section].suggestions[indexPath.row];

        UITableViewCell* cell;
        OmniboxPopupRowContentConfiguration* configuration;

        if (base::FeatureList::IsEnabled(kOmniboxActionsInSuggest) &&
            suggestion.actionsInSuggest.count > 0) {
          cell = [self.tableView dequeueReusableCellWithIdentifier:
                                     OmniboxPopupActionsRowCellReuseIdentifier
                                                      forIndexPath:indexPath];
          configuration =
              [OmniboxPopupActionsRowContentConfiguration cellConfiguration];
        } else {
          cell = [self.tableView dequeueReusableCellWithIdentifier:
                                     OmniboxPopupRowCellReuseIdentifier
                                                      forIndexPath:indexPath];
          configuration =
              [OmniboxPopupRowContentConfiguration cellConfiguration];
        }

        DCHECK(cell);
        DCHECK(configuration);
        configuration.suggestion = suggestion;
        configuration.delegate = self;
        configuration.indexPath = indexPath;
        configuration.showSeparator =
            (NSUInteger)indexPath.row <
            self.currentResult[indexPath.section].suggestions.count - 1;
        configuration.semanticContentAttribute = self.semanticContentAttribute;
        configuration.faviconRetriever = self.faviconRetriever;
        configuration.imageRetriever = self.imageRetriever;

        [cell setContentConfiguration:configuration];
        cell.backgroundConfiguration =
            [UIBackgroundConfiguration clearConfiguration];
        return cell;
    }
    case SuggestionGroupDisplayStyleCarousel: {
      NSArray<CarouselItem*>* carouselItems = [self
          carouselItemsFromSuggestionGroup:self.currentResult[indexPath.section]
                            groupIndexPath:indexPath];
      [self.carouselCell setupWithCarouselItems:carouselItems];
      for (CarouselItem* item in carouselItems) {
        [self fetchFaviconForCarouselItem:item];
      }
      return self.carouselCell;
    }
  }
}

#pragma mark - OmniboxPopupCarouselCellDelegate

- (void)carouselCellDidChangeItemCount:(OmniboxPopupCarouselCell*)carouselCell {
  if (carouselCell.tileCount == 0) {
    // Hide the carousel row.
    self.shouldHideCarousel = YES;
    NSInteger carouselSection =
        [self.tableView indexPathForCell:self.carouselCell].section;
    [self.tableView
        deleteRowsAtIndexPaths:@[ [NSIndexPath
                                   indexPathForRow:0
                                         inSection:carouselSection] ]
              withRowAnimation:UITableViewRowAnimationAutomatic];
    [self resetHighlighting];
    return;
  }

  if (self.highlightedIndexPath.section !=
      [self.tableView indexPathForCell:self.carouselCell].section) {
    return;
  }

  // Defensively update highlightedIndexPath, because the highlighted tile might
  // have been removed.
  NSInteger highlightedTileIndex = self.carouselCell.highlightedTileIndex;
  if (highlightedTileIndex == NSNotFound) {
    [self resetHighlighting];
  } else {
    self.highlightedIndexPath =
        [NSIndexPath indexPathForRow:highlightedTileIndex
                           inSection:self.highlightedIndexPath.section];
  }
}

- (void)carouselCell:(OmniboxPopupCarouselCell*)carouselCell
    didTapCarouselItem:(CarouselItem*)carouselItem {
  id<AutocompleteSuggestion> suggestion =
      [self suggestionAtIndexPath:carouselItem.indexPath];
  DCHECK(suggestion);

  NSInteger absoluteRow =
      [self absoluteRowIndexForIndexPath:carouselItem.indexPath];
  [self.delegate autocompleteResultConsumer:self
                        didSelectSuggestion:suggestion
                                      inRow:absoluteRow];
}

#pragma mark - Internal API methods

/// Reset the highlighting to the first suggestion when it's available. Reset
/// to nil otherwise.
- (void)resetHighlighting {
  if (self.currentResult.firstObject.suggestions.count > 0) {
    self.highlightedIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
  } else {
    self.highlightedIndexPath = nil;
  }
}

/// Updates the color of the background based on the incognito-ness and the size
/// class.
- (void)updateBackgroundColor {
  if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    self.view.backgroundColor = [UIColor colorNamed:kPrimaryBackgroundColor];
    return;
  }

  self.view.backgroundColor = [UIColor clearColor];
}

#pragma mark - UIScrollViewDelegate

- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
  // TODO(crbug.com/41325585): Default to the dragging check once it's been
  // tested on trunk.
  if (!scrollView.dragging)
    return;

  // TODO(crbug.com/40604984): The following call chain ultimately just
  // dismisses the keyboard, but involves many layers of plumbing, and should be
  // refactored.
  if (self.forwardsScrollEvents)
    [self.delegate autocompleteResultConsumerDidScroll:self];

  [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow
                                animated:NO];
}

#pragma mark - Keyboard events

- (void)keyboardDidChangeFrame:(NSNotification*)notification {
  CGFloat keyboardHeight =
      [KeyboardObserverHelper keyboardHeightInWindow:self.tableView.window];
  if (self.keyboardHeight != keyboardHeight) {
    self.keyboardHeight = keyboardHeight;
    self.shouldUpdateVisibleSuggestionCount = YES;
  }
}

#pragma mark - Content size events

- (void)contentSizeDidChange:(NSNotification*)notification {
  self.shouldUpdateVisibleSuggestionCount = YES;
}

#pragma mark - ContentProviding

- (BOOL)hasContent {
  return self.tableView.numberOfSections > 0 &&
         [self.tableView numberOfRowsInSection:0] > 0;
}

#pragma mark - CarouselItemConsumer

- (void)deleteCarouselItem:(CarouselItem*)carouselItem {
  [self.carouselCell deleteCarouselItem:carouselItem];
}

#pragma mark - Private Methods

- (id<AutocompleteSuggestion>)suggestionAtIndexPath:(NSIndexPath*)indexPath {
  if (indexPath.section < 0 || indexPath.row < 0) {
    return nil;
  }
  if (!self.currentResult || self.currentResult.count == 0 ||
      self.currentResult.count <= (NSUInteger)indexPath.section ||
      self.currentResult[indexPath.section].suggestions.count <=
          (NSUInteger)indexPath.row) {
    return nil;
  }
  return self.currentResult[indexPath.section].suggestions[indexPath.row];
}

/// Returns the absolute row number for `indexPath`, counting every row in every
/// section above. Used for logging.
- (NSInteger)absoluteRowIndexForIndexPath:(NSIndexPath*)indexPath {
  if (![self suggestionAtIndexPath:indexPath]) {
    return NSNotFound;
  }
  NSInteger rowCount = 0;
  // For each section above `indexPath` add the number of row used by the
  // section.
  for (NSInteger i = 0; i < indexPath.section; ++i) {
    rowCount += [self.tableView numberOfRowsInSection:i];
  }
  switch (self.currentResult[indexPath.section].displayStyle) {
    case SuggestionGroupDisplayStyleDefault:
      return rowCount + indexPath.row;
    case SuggestionGroupDisplayStyleCarousel:
      return rowCount;
  }
}

- (void)updateVisibleSuggestionCount {
  CGRect tableViewFrameInCurrentWindowCoordinateSpace =
      [self.tableView convertRect:self.tableView.bounds
                toCoordinateSpace:self.tableView.window.coordinateSpace];
  // Computes the visible area between the omnibox and the keyboard.
  CGFloat visibleTableViewHeight =
      CGRectGetHeight(self.tableView.window.bounds) -
      tableViewFrameInCurrentWindowCoordinateSpace.origin.y -
      self.keyboardHeight - self.tableView.contentInset.top;
  // Use font size to estimate the size of a omnibox search suggestion.
  CGFloat fontSizeHeight = [@"T" sizeWithAttributes:@{
                             NSFontAttributeName : [UIFont
                                 preferredFontForTextStyle:UIFontTextStyleBody]
                           }]
                               .height;
  // Add padding to the estimated row height and set its minimum to be at
  // `kOmniboxPopupCellMinimumHeight`.
  CGFloat estimatedRowHeight =
      MAX(fontSizeHeight + 2 * kBottomPadding, kOmniboxPopupCellMinimumHeight);
  CGFloat visibleRows = visibleTableViewHeight / estimatedRowHeight;
  // A row is considered visible if `kVisibleSuggestionTreshold` percent of its
  // height is visible.
  self.visibleSuggestionCount =
      floor(visibleRows + (1.0 - kVisibleSuggestionThreshold));
  self.shouldUpdateVisibleSuggestionCount = NO;
}

- (NSArray<CarouselItem*>*)
    carouselItemsFromSuggestionGroup:(id<AutocompleteSuggestionGroup>)group
                      groupIndexPath:(NSIndexPath*)groupIndexPath {
  NSMutableArray* carouselItems =
      [[NSMutableArray alloc] initWithCapacity:group.suggestions.count];
  for (NSUInteger i = 0; i < group.suggestions.count; ++i) {
    id<AutocompleteSuggestion> suggestion = group.suggestions[i];
    NSIndexPath* itemIndexPath =
        [NSIndexPath indexPathForRow:i inSection:groupIndexPath.section];
    CarouselItem* item = [[CarouselItem alloc] init];
    item.title = suggestion.text.string;
    item.indexPath = itemIndexPath;
    item.URL = suggestion.destinationUrl;
    [carouselItems addObject:item];
  }
  return carouselItems;
}

// TODO(crbug.com/40866206): Move to a mediator.
- (void)fetchFaviconForCarouselItem:(CarouselItem*)carouselItem {
  __weak OmniboxPopupCarouselCell* weakCell = self.carouselCell;
  __weak CarouselItem* weakItem = carouselItem;

  void (^completion)(FaviconAttributes*) = ^(FaviconAttributes* attributes) {
    OmniboxPopupCarouselCell* strongCell = weakCell;
    CarouselItem* strongItem = weakItem;
    if (!strongCell || !strongItem)
      return;

    strongItem.faviconAttributes = attributes;
    [strongCell updateCarouselItem:strongItem];
  };

  [self.carouselAttributeProvider
      fetchFaviconAttributesForURL:carouselItem.URL.gurl
                        completion:completion];
}

- (void)showDebugUI {
  [self presentViewController:self.debugInfoViewController
                     animated:YES
                   completion:nil];
}

- (UILayoutGuide*)omniboxGuide {
  if (!_omniboxGuide) {
    _omniboxGuide =
        [self.layoutGuideCenter makeLayoutGuideNamed:kTopOmniboxGuide];
    [self.view addLayoutGuide:_omniboxGuide];
  }
  return _omniboxGuide;
}

@end