// Copyright 2017 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/bookmarks/ui_bundled/home/bookmarks_home_view_controller.h"

#import <algorithm>
#import <set>

#import "base/apple/foundation_util.h"
#import "base/i18n/message_formatter.h"
#import "base/ios/ios_util.h"
#import "base/memory/raw_ptr.h"
#import "base/memory/weak_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/numerics/safe_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/bookmark_node.h"
#import "components/bookmarks/browser/bookmark_utils.h"
#import "components/bookmarks/common/bookmark_features.h"
#import "components/bookmarks/common/bookmark_metrics.h"
#import "components/bookmarks/common/bookmark_pref_names.h"
#import "components/bookmarks/managed/managed_bookmark_service.h"
#import "components/prefs/pref_service.h"
#import "components/send_tab_to_self/features.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/app/tests_hook.h"
#import "ios/chrome/browser/authentication/ui_bundled/cells/signin_promo_view_configurator.h"
#import "ios/chrome/browser/authentication/ui_bundled/cells/table_view_signin_promo_item.h"
#import "ios/chrome/browser/authentication/ui_bundled/signin/signin_coordinator.h"
#import "ios/chrome/browser/authentication/ui_bundled/signin/signin_utils.h"
#import "ios/chrome/browser/bookmarks/editor/coordinator/bookmarks_editor_coordinator.h"
#import "ios/chrome/browser/bookmarks/editor/coordinator/bookmarks_editor_coordinator_delegate.h"
#import "ios/chrome/browser/bookmarks/folder_chooser/coordinator/bookmarks_folder_chooser_coordinator.h"
#import "ios/chrome/browser/bookmarks/folder_chooser/coordinator/bookmarks_folder_chooser_coordinator_delegate.h"
#import "ios/chrome/browser/bookmarks/folder_chooser/ui/table_view_bookmarks_folder_item.h"
#import "ios/chrome/browser/bookmarks/folder_editor/coordinator/bookmarks_folder_editor_coordinator.h"
#import "ios/chrome/browser/bookmarks/folder_editor/coordinator/bookmarks_folder_editor_coordinator_delegate.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_bridge_observer.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_storage_type.h"
#import "ios/chrome/browser/bookmarks/model/bookmarks_utils.h"
#import "ios/chrome/browser/bookmarks/model/managed_bookmark_service_factory.h"
#import "ios/chrome/browser/bookmarks/public/bookmarks_ui_constants.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_navigation_controller.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_path_cache.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_utils_ios.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/cells/bookmark_table_cell_title_edit_delegate.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/home/bookmarks_home_consumer.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/home/bookmarks_home_mediator.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/home/bookmarks_home_node_item.h"
#import "ios/chrome/browser/drag_and_drop/model/drag_item_util.h"
#import "ios/chrome/browser/drag_and_drop/model/table_view_url_drag_drop_handler.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/incognito_reauth/ui_bundled/incognito_reauth_scene_agent.h"
#import "ios/chrome/browser/intents/model/intents_donation_helper.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/menu/ui_bundled/browser_action_factory.h"
#import "ios/chrome/browser/menu/ui_bundled/menu_histograms.h"
#import "ios/chrome/browser/metrics/model/new_tab_page_uma.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h"
#import "ios/chrome/browser/shared/coordinator/alert/alert_coordinator.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/coordinator/scene/state/incognito_state.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider_interface.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/scene_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/commands/show_signin_command.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/elements/home_waiting_view.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_url_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_illustrated_empty_view.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_model.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_navigation_controller_constants.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/pasteboard_util.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/sharing/ui_bundled/sharing_coordinator.h"
#import "ios/chrome/browser/sharing/ui_bundled/sharing_params.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/chrome/browser/window_activities/model/window_activity_helpers.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "ios/chrome/common/ui/favicon/favicon_constants.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/referrer.h"
#import "ios/web/public/web_state.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/strings/grit/ui_strings.h"

namespace {

using bookmarks::BookmarkNode;
using l10n_util::GetNSString;

using BookmarkNodeIDSet = std::set<int64_t>;

// Used to store a pair of NSIntegers when storing a NSIndexPath in C++
// collections.
using IntegerPair = std::pair<NSInteger, NSInteger>;

typedef NS_ENUM(NSInteger, BookmarksContextBarState) {
  BookmarksContextBarNone,            // No state.
  BookmarksContextBarDefault,         // No selection is possible in this state.
  BookmarksContextBarBeginSelection,  // This is the clean start state,
  // selection is possible, but nothing is
  // selected yet.
  BookmarksContextBarSingleURLSelection,       // Single URL selected state.
  BookmarksContextBarMultipleURLSelection,     // Multiple URLs selected state.
  BookmarksContextBarSingleFolderSelection,    // Single folder selected.
  BookmarksContextBarMultipleFolderSelection,  // Multiple folders selected.
  BookmarksContextBarMixedSelection,  // Multiple URL / Folders selected.
};

// Separation between non-empty account and profile sections.
constexpr CGFloat kSpaceBetweenAccountAndProfileSections = 32.0;

// Returns a vector of all URLs in `nodes`.
std::vector<GURL> GetUrlsToOpen(const std::vector<const BookmarkNode*>& nodes) {
  std::vector<GURL> urls;
  for (const BookmarkNode* node : nodes) {
    if (node->is_url()) {
      urls.push_back(node->url());
    }
  }
  return urls;
}

// Given a set of BookmarkNode pointers, it returns their IDs in a set.
BookmarkNodeIDSet GetBookmarkNodeIDSet(
    const std::set<const BookmarkNode*>& nodes) {
  BookmarkNodeIDSet nodeIDs;
  for (const BookmarkNode* node : nodes) {
    nodeIDs.emplace(node->id());
  }
  return nodeIDs;
}

}  // namespace

@interface BookmarksHomeViewController () <
    BookmarkModelBridgeObserver,
    BookmarksEditorCoordinatorDelegate,
    BookmarksFolderChooserCoordinatorDelegate,
    BookmarksFolderEditorCoordinatorDelegate,
    BookmarksHomeConsumer,
    BookmarkTableCellTitleEditDelegate,
    TableViewURLDragDataSource,
    TableViewURLDropDelegate,
    UIGestureRecognizerDelegate,
    UISearchControllerDelegate,
    UISearchResultsUpdating,
    UITableViewDataSource,
    UITableViewDelegate>

// The mediator that provides data for this view controller.
@property(nonatomic, strong) BookmarksHomeMediator* mediator;

// TODO(crbug.com/40251259): Move this to BookmarksHomeCoordinator.
// A reference to the presented folder chooser.
@property(nonatomic, strong)
    BookmarksFolderChooserCoordinator* folderChooserCoordinator;

// A reference to the potentially presented single bookmark editor.
@property(nonatomic, strong)
    BookmarksEditorCoordinator* bookmarkEditorCoordinator;

// A reference to the potentially presented folder editor.
@property(nonatomic, strong)
    BookmarksFolderEditorCoordinator* folderEditorCoordinator;

// FaviconLoader is a keyed service that uses LargeIconService to retrieve
// favicon images.
@property(nonatomic, assign) FaviconLoader* faviconLoader;

// The current state of the context bar UI.
@property(nonatomic, assign) BookmarksContextBarState contextBarState;

// When the view is first shown on the screen, this property represents the
// cached value of the top most visible indexPath row of the table view. This
// property is set to nil after it is used.
@property(nonatomic, assign) int cachedIndexPathRow;

// Set to YES, only when this view controller instance is being created
// from cached path. Once the view controller is shown, this is set to NO.
// This is so that the cache code is called only once in loadBookmarkViews.
@property(nonatomic, assign) BOOL isReconstructingFromCache;

// The current search term.  Set to the empty string when no search is active.
@property(nonatomic, copy) NSString* searchTerm;

// This ViewController's searchController;
@property(nonatomic, strong) UISearchController* searchController;

// Navigation UIToolbar Delete button.
@property(nonatomic, strong) UIBarButtonItem* deleteButton;

// Navigation UIToolbar More button.
@property(nonatomic, strong) UIBarButtonItem* moreButton;

// Scrim when search box in focused.
@property(nonatomic, strong) UIControl* scrimView;

// Illustrated View displayed when the current root node is empty.
@property(nonatomic, strong) TableViewIllustratedEmptyView* emptyViewBackground;

// The loading spinner background which appears when loading the BookmarkModel
// or syncing.
@property(nonatomic, strong) HomeWaitingView* spinnerView;

// The action sheet coordinator, if one is currently being shown.
@property(nonatomic, strong) AlertCoordinator* actionSheetCoordinator;

@property(nonatomic, assign) WebStateList* webStateList;

// Handler for URL drag and drop interactions.
@property(nonatomic, strong) TableViewURLDragDropHandler* dragDropHandler;

// Coordinator in charge of handling sharing use cases.
@property(nonatomic, strong) SharingCoordinator* sharingCoordinator;

@end

@implementation BookmarksHomeViewController {
  // The bookmark model used.
  base::WeakPtr<bookmarks::BookmarkModel> _bookmarkModel;
  // The Browser in which bookmarks are presented
  base::WeakPtr<Browser> _browser;
  // Bridge to register for bookmark changes.
  std::unique_ptr<BookmarkModelBridge> _bookmarkModelBridge;
  // The bookmark node that was choosen by an entity outside of the Bookmarks UI
  // and is selected when the view is loaded.
  raw_ptr<const BookmarkNode> _externalBookmark;
  // Whether the view controller was requested to shutdown.
  BOOL _isShutDown;
  // Whether the navigation controller is being dismissed.
  // In which case, do not open anything on top of it.
  BOOL _isBeingDismissed;
  // The Signin coordinator displayed, if any.
  SigninCoordinator* _signinCoordinator;
  // Whether the UI is disabled.
  BOOL _UIDisabled;
  // The bookmark folder currently being displayed.
  raw_ptr<const bookmarks::BookmarkNode> _displayedFolderNode;
}

@synthesize editingFolderCell = _editingFolderCell;

- (instancetype)initWithBrowser:(Browser*)browser {
  CHECK(browser, base::NotFatalUntil::M152);
  UITableViewStyle style = ChromeTableViewStyle();
  self = [super initWithStyle:style];
  if (self) {
    _browser = browser->AsWeakPtr();
    ProfileIOS* profile = self.profile;
    _webStateList = browser->GetWebStateList();

    _faviconLoader = IOSChromeFaviconLoaderFactory::GetForProfile(profile);

    _bookmarkModel =
        ios::BookmarkModelFactory::GetForProfile(profile)->AsWeakPtr();
    _bookmarkModelBridge =
        std::make_unique<BookmarkModelBridge>(self, _bookmarkModel.get());
  }
  return self;
}

- (void)dealloc {
  [self shutdown];
}

- (void)shutdown {
  if (_isShutDown) {
    return;
  }
  _isShutDown = YES;
  [self stopSigninCoordinator];
  [self.editingFolderCell stopEdit];
  [self stopFolderChooserCoordinator];
  [self stopBookmarksEditorCoordinator];
  [self stopBookmarksFolderEditorCoordinator];
  [self.mediator disconnect];
  _displayedFolderNode = nullptr;
  self.mediator.consumer = nil;
  self.mediator = nil;
  _browser = nullptr;
  [self.searchController dismissViewControllerAnimated:YES completion:nil];
  [self dismissActionSheetCoordinator];
  _bookmarkModel = nullptr;
  _bookmarkModelBridge.reset();
  [self.sharingCoordinator stop];
  self.sharingCoordinator = nil;
}

- (BOOL)canDismiss {
  if (self.mediator && ![self.mediator canDismiss]) {
    return NO;
  }
  if (self.folderChooserCoordinator &&
      ![self.folderChooserCoordinator canDismiss]) {
    return NO;
  }
  if (self.bookmarkEditorCoordinator &&
      ![self.bookmarkEditorCoordinator canDismiss]) {
    return NO;
  }
  if (self.folderEditorCoordinator &&
      ![self.folderEditorCoordinator canDismiss]) {
    return NO;
  }
  return YES;
}

- (NSArray<BookmarksHomeViewController*>*)cachedViewControllerStack {
  // This method is only designed to be called for the view controller
  // associated with the root node.
  CHECK(_bookmarkModel->loaded());
  CHECK([self isDisplayingBookmarkRoot], base::NotFatalUntil::M152);
  // The displayed node is the root node, so it must still exists.
  CHECK(self.displayedFolderNode, base::NotFatalUntil::M152);

  NSMutableArray<BookmarksHomeViewController*>* stack = [NSMutableArray array];
  // Configure the root controller Navigationbar at this time when
  // reconstructing from cache, or there will be a loading flicker if this gets
  // done on viewDidLoad.
  [self setupNavigationForBookmarksHomeViewController:self
                                    usingBookmarkNode:self.displayedFolderNode];
  [stack addObject:self];

  int64_t cachedFolderID;
  int cachedIndexPathRow;
  // If cache is present then reconstruct the last visited bookmark from
  // cache.
  if (![BookmarkPathCache
          bookmarkTopMostRowCacheWithPrefService:self.profile->GetPrefs()
                                   bookmarkModel:_bookmarkModel.get()
                                        folderId:&cachedFolderID
                                      topMostRow:&cachedIndexPathRow] ||
      cachedFolderID == _bookmarkModel->root_node()->id()) {
    return stack;
  }

  NSArray<NSNumber*>* path = bookmark_utils_ios::CreateBookmarkPath(
      _bookmarkModel.get(), cachedFolderID);
  if (!path) {
    return stack;
  }

  for (NSUInteger ii = 0; ii < [path count]; ii++) {
    int64_t nodeID = [[path objectAtIndex:ii] longLongValue];
    const BookmarkNode* node =
        bookmark_utils_ios::FindFolderById(_bookmarkModel.get(), nodeID);
    CHECK(node, base::NotFatalUntil::M152);
    // if node is an empty permanent node, stop.
    if (node->children().empty() && node->is_permanent_node()) {
      break;
    }

    BookmarksHomeViewController* controller =
        [self createControllerWithDisplayedFolderNode:node];
    // Configure the controller's Navigationbar at this time when
    // reconstructing from cache, or there will be a loading flicker if this
    // gets done on viewDidLoad.
    [self setupNavigationForBookmarksHomeViewController:controller
                                      usingBookmarkNode:node];
    if (nodeID == cachedFolderID) {
      controller.cachedIndexPathRow = cachedIndexPathRow;
    }
    [stack addObject:controller];
  }
  return stack;
}

- (void)willDismiss {
  _isBeingDismissed = YES;
}

- (void)willDismissBySwipeDown {
  if (self.searchController.active) {
    // Dismiss the keyboard if trying to dismiss the VC so the keyboard doesn't
    // linger until the VC dismissal has completed.
    [self.searchController.searchBar endEditing:YES];
  }
  [self willDismiss];
}

- (const bookmarks::BookmarkNode*)displayedFolderNode {
  return _displayedFolderNode;
}

- (void)setDisplayedFolderNode:
    (const bookmarks::BookmarkNode*)displayedFolderNode {
  _displayedFolderNode = displayedFolderNode;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  if (!_bookmarkModel) {
    return;
  }

  // Set Navigation Bar, Toolbar and TableView appearance.
  self.navigationController.navigationBarHidden = NO;

  self.navigationController.toolbar.accessibilityIdentifier =
      kBookmarksHomeUIToolbarIdentifier;

  // SearchController Configuration.
  // Init the searchController with nil so the results are displayed on the
  // same TableView.
  self.searchController =
      [[UISearchController alloc] initWithSearchResultsController:nil];
  self.searchController.obscuresBackgroundDuringPresentation = NO;
  self.searchController.searchBar.userInteractionEnabled = NO;
  self.searchController.delegate = self;
  self.searchController.searchResultsUpdater = self;
  self.searchController.searchBar.backgroundColor = UIColor.clearColor;
  self.searchController.searchBar.accessibilityIdentifier =
      kBookmarksHomeSearchBarIdentifier;

  // UIKit needs to know which controller will be presenting the
  // searchController. If we don't add this trying to dismiss while
  // SearchController is active will fail.
  self.definesPresentationContext = YES;

  self.scrimView = [[UIControl alloc] init];
  self.scrimView.backgroundColor = [UIColor colorNamed:kScrimBackgroundColor];
  self.scrimView.translatesAutoresizingMaskIntoConstraints = NO;
  self.scrimView.accessibilityIdentifier = kBookmarksHomeSearchScrimIdentifier;
  [self.scrimView addTarget:self
                     action:@selector(dismissSearchController:)
           forControlEvents:UIControlEventTouchUpInside];

  // Place the search bar in the navigation bar.
  self.navigationItem.searchController = self.searchController;
  if (@available(iOS 26, *)) {
    self.navigationItem.searchBarPlacementAllowsToolbarIntegration = NO;
  }
  self.navigationItem.hidesSearchBarWhenScrolling = NO;

  self.searchTerm = @"";

  if (_bookmarkModel->loaded()) {
    [self loadBookmarkViews];
  } else {
    [self showLoadingSpinnerBackground];
  }

  [self registerForTraitChanges:TraitCollectionSetForTraits(nil)
                     withAction:@selector(stopEdittingBookmarkOnTraitChange)];
}

- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];

  if (_isShutDown || !_bookmarkModel) {
    // After `shutdown` is called, `_profile` is null.
    return;
  }
  // Set the delegate here to make sure it is working when navigating in the
  // ViewController hierarchy (as each view controller is setting itself as
  // delegate).
  self.navigationController.interactivePopGestureRecognizer.delegate = self;

  // Hide the toolbar if we're displaying the root node.
  if (_bookmarkModel->loaded() &&
      (![self isDisplayingBookmarkRoot] ||
       self.mediator.currentlyShowingSearchResults)) {
    self.navigationController.toolbarHidden = NO;
  } else {
    self.navigationController.toolbarHidden = YES;
  }

  // If we navigate back to the root level, we need to make sure the root level
  // folders are created or deleted if needed.
  if ([self isDisplayingBookmarkRoot]) {
    [self refreshContents];
  }

  [IntentDonationHelper donateIntent:IntentType::kOpenBookmarks];
}

// Note: Unlike a standard trio of mediator-coordinator-viewcontroller,
// multiple `BookmarksHomeViewController` instances are pushed for subfolder
// navigation under a single coordinator and a single mediator.
// `viewDidDisappear:` cannot be used for shutdown as subfolder navigation
// would prematurely destroy the coordinator's shared mediator. Teardown is
// instead tied to root container removal (`parent == nil`).
- (void)didMoveToParentViewController:(UIViewController*)parent {
  [super didMoveToParentViewController:parent];
  if (_isShutDown) {
    return;
  }

  if (parent) {
    // This view controller is added to the navigation controller.
    return;
  }

  // This view controller is removed from its parent, shutdown can be done.
  [self shutdown];
}

- (void)viewDidLayoutSubviews {
  [super viewDidLayoutSubviews];
  // Check that the tableView still contains as many rows, and that
  // `self.cachedIndexPathRow` is not 0.
  if (self.cachedIndexPathRow &&
      [self.tableView numberOfRowsInSection:0] > self.cachedIndexPathRow) {
    NSIndexPath* indexPath =
        [NSIndexPath indexPathForRow:self.cachedIndexPathRow inSection:0];
    [self.tableView scrollToRowAtIndexPath:indexPath
                          atScrollPosition:UITableViewScrollPositionTop
                                  animated:NO];
    self.cachedIndexPathRow = 0;
  }
}

- (BOOL)prefersStatusBarHidden {
  return NO;
}

- (UIStatusBarStyle)preferredStatusBarStyle {
  return UIStatusBarStyleDefault;
}

#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;
}

- (NSArray<UIKeyCommand*>*)keyCommands {
  return @[ UIKeyCommand.cr_close ];
}

- (void)keyCommand_close {
  base::RecordAction(base::UserMetricsAction(kMobileKeyCommandClose));
  [self navigationBarCancel:nil];
}

#pragma mark - Protected

- (void)loadBookmarkViews {
  if (!self.displayedFolderNode) {
    // The view is being dismissed before being loaded, so let’s not load it.
    return;
  }
  [self loadModel];

  self.dragDropHandler = [[TableViewURLDragDropHandler alloc] init];
  self.dragDropHandler.origin = WindowActivityBookmarksOrigin;
  self.dragDropHandler.dragDataSource = self;
  self.dragDropHandler.dropDelegate = self;
  self.tableView.dragDelegate = self.dragDropHandler;
  self.tableView.dropDelegate = self.dragDropHandler;
  self.tableView.dragInteractionEnabled = true;

  self.tableView.accessibilityIdentifier = kBookmarksHomeTableViewIdentifier;
  self.tableView.allowsMultipleSelectionDuringEditing = YES;

  // Create the mediator and hook up the table view.
  self.mediator =
      [[BookmarksHomeMediator alloc] initWithBrowser:_browser.get()
                                       bookmarkModel:_bookmarkModel.get()
                                       displayedNode:self.displayedFolderNode];
  self.mediator.currentlyShowingSearchResults = NO;
  // Configure the table view.
  self.mediator.consumer = self;
  [self.mediator startMediating];

  [self setupNavigationForBookmarksHomeViewController:self
                                    usingBookmarkNode:self.displayedFolderNode];

  [self setupContextBar];

  if (self.isReconstructingFromCache) {
    [self setupUIStackCacheIfApplicable];
  }

  self.searchController.searchBar.userInteractionEnabled = YES;

  [self editExternalBookmarkIfSet];

  CHECK(_bookmarkModel->loaded(), base::NotFatalUntil::M152);
  CHECK([self isViewLoaded], base::NotFatalUntil::M152);
}

- (void)cacheIndexPathRow {
  // Cache IndexPathRow for BookmarkTableView.
  int topMostVisibleIndexPathRow = [self topMostVisibleIndexPathRow];
  if (self.displayedFolderNode && self.profile) {
    [BookmarkPathCache
        cacheBookmarkTopMostRowWithPrefService:self.profile->GetPrefs()
                                      folderId:self.displayedFolderNode->id()
                                     inStorage:bookmark_utils_ios::
                                                   GetBookmarkStorageType(
                                                       self.displayedFolderNode,
                                                       _bookmarkModel.get())
                                    topMostRow:topMostVisibleIndexPathRow];
  }
}

#pragma mark - BookmarksHomeConsumer

- (void)closeThisFolder {
  if (!self.displayedFolderNode) {
    // The view is being dismissed before being loaded, so no need to close it
    // again.
    return;
  }
  [self jumpToFolder:self.displayedFolderNode->parent()];
}

- (void)displayRoot {
  [self jumpToFolder:_bookmarkModel->root_node()];
}

- (void)setTableViewEditing:(BOOL)editing {
  self.mediator.currentlyInEditMode = editing;
  [self setContextBarState:editing ? BookmarksContextBarBeginSelection
                                   : BookmarksContextBarDefault];
  self.searchController.searchBar.userInteractionEnabled = !editing;
  self.searchController.searchBar.alpha =
      editing ? kTableViewNavigationAlphaForDisabledSearchBar : 1.0;

  self.tableView.dragInteractionEnabled = !editing;

  self.navigationItem.rightBarButtonItem = [self createNavigationBarDoneButton];
}

- (void)refreshContents {
  if (self.mediator.currentlyShowingSearchResults) {
    NSString* noResults = GetNSString(IDS_HISTORY_NO_SEARCH_RESULTS);
    [self.mediator computeBookmarkTableViewDataMatching:self.searchTerm
                             orShowMessageWhenNoResults:noResults];
  } else {
    [self.mediator computeBookmarkTableViewData];
  }
  [self handleRefreshContextBar];
  [self.editingFolderCell stopEdit];
  [self.tableView.contextMenuInteraction dismissMenu];
  [self.tableView reloadData];
  if (self.mediator.currentlyInEditMode &&
      !self.mediator.selectedNodesForEditMode.empty()) {
    [self restoreRowSelection];
  }
}

- (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath
        fallbackToGoogleServer:(BOOL)fallbackToGoogleServer {
  const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
  if (!node || node->is_folder()) {
    return;
  }

  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
  BookmarksHomeNodeItem* URLItem =
      base::apple::ObjCCastStrict<BookmarksHomeNodeItem>(item);

  // Start loading a favicon.
  int64_t nodeId = node->id();
  __weak BookmarksHomeViewController* weakSelf = self;
  auto faviconLoadedBlock = ^(FaviconAttributes* attributes, bool cached) {
    [weakSelf didFetchFaviconAttributes:attributes
                                 cached:cached
                                   item:URLItem
                              indexPath:indexPath
                                 nodeId:nodeId];
  };

  GURL blockURL(node->url());
  self.faviconLoader->FaviconForPageUrl(
      blockURL, kDesiredMediumFaviconSizePt, kMinFaviconSizePt,
      /*fallback_to_google_server=*/fallbackToGoogleServer, faviconLoadedBlock);
}

- (void)updateTableViewBackgroundStyle:(BookmarksHomeBackgroundStyle)style {
  if (style == BookmarksHomeBackgroundStyleDefault) {
    [self hideLoadingSpinnerBackground];
    [self hideEmptyBackground];
  } else if (style == BookmarksHomeBackgroundStyleLoading) {
    [self hideEmptyBackground];
    [self showLoadingSpinnerBackground];
  } else if (style == BookmarksHomeBackgroundStyleEmpty) {
    [self hideLoadingSpinnerBackground];
    [self showEmptyBackground];
  }
}

- (void)showSignin:(ShowSigninCommand*)command {
  if (_signinCoordinator.viewWillPersist) {
    return;
  }
  [_signinCoordinator stop];
  __weak __typeof(self) weakSelf = self;
  [command addSigninCompletion:^(SigninCoordinator* coordinator,
                                 SigninCoordinatorResult result,
                                 id<SystemIdentity>) {
    [weakSelf signinDidCompleteWithCoordinator:coordinator result:result];
  }];
  _signinCoordinator = [SigninCoordinator
      signinCoordinatorWithCommand:command
                           browser:signin::GetRegularBrowser(_browser.get())
                baseViewController:self.navigationController];
  [_signinCoordinator start];
}

- (void)configureSigninPromoWithConfigurator:
            (SigninPromoViewConfigurator*)configurator
                                 atIndexPath:(NSIndexPath*)indexPath {
  TableViewSigninPromoItem* signinPromoItem =
      base::apple::ObjCCast<TableViewSigninPromoItem>(
          [self.tableViewModel itemAtIndexPath:indexPath]);
  if (!signinPromoItem) {
    return;
  }

  signinPromoItem.configurator = configurator;
  [self reloadCellsForItems:@[ signinPromoItem ]
           withRowAnimation:UITableViewRowAnimationNone];
}

- (void)mediatorDidClearEditNodes:(BookmarksHomeMediator*)mediator {
  [self handleSelectEditNodes:mediator.selectedNodesForEditMode];
}

- (void)showAccountSettings {
  id<SettingsCommands> settingsHandler =
      HandlerForProtocol(_browser->GetCommandDispatcher(), SettingsCommands);
  [settingsHandler showSyncSettingsFromViewController:self];
}

#pragma mark - BookmarksHomeConsumer Helper

- (void)signinDidCompleteWithCoordinator:(SigninCoordinator*)coordinator
                                  result:(SigninCoordinatorResult)result {
  CHECK_EQ(_signinCoordinator, coordinator, base::NotFatalUntil::M151);
  [self.mediator signinDidCompleteWithResult:result];
  [self stopSigninCoordinator];
}

#pragma mark - Action sheet callbacks

// Returns contextual menu for a bookmark node at `indexPath`.
- (UIMenu*)bookmarkNodeContextualMenuWithIndexPath:(NSIndexPath*)indexPath
                                       canEditNode:(BOOL)canEditNode {
  const BookmarkNode* bookmarkNode = [self nodeAtIndexPath:indexPath];
  DCHECK_EQ(bookmarkNode->type(), BookmarkNode::URL);
  const GURL nodeURL = bookmarkNode->url();
  const int64_t nodeID = bookmarkNode->id();

  // Record that this context menu was shown to the user.
  RecordMenuShown(kMenuScenarioHistogramBookmarkEntry);
  BrowserActionFactory* actionFactory = [[BrowserActionFactory alloc]
      initWithBrowser:_browser.get()
             scenario:kMenuScenarioHistogramBookmarkEntry];
  NSMutableArray<UIMenuElement*>* menuElements = [[NSMutableArray alloc] init];
  __weak __typeof(self) weakSelf = self;
  // Add open URL menu item.
  UIAction* openAction = [actionFactory actionToOpenInNewTabWithBlock:^{
    [weakSelf openAllURLs:{nodeURL} inIncognito:NO newTab:YES];
  }];
  if ([self isIncognitoForced]) {
    openAction.attributes = UIMenuElementAttributesDisabled;
  }
  [menuElements addObject:openAction];
  // Add open URL in incognito menu item.
  UIAction* openInIncognito =
      [actionFactory actionToOpenInNewIncognitoTabWithBlock:^{
        [weakSelf openAllURLs:{nodeURL} inIncognito:YES newTab:YES];
      }];
  if (![self isIncognitoAvailable]) {
    openInIncognito.attributes = UIMenuElementAttributesDisabled;
  }
  [menuElements addObject:openInIncognito];
  // Add open URL in new window menu item.
  if (base::ios::IsMultipleScenesSupported()) {
    [menuElements
        addObject:
            [actionFactory
                actionToOpenInNewWindowWithURL:nodeURL
                                activityOrigin:WindowActivityBookmarksOrigin]];
  }
  [menuElements
      addObject:[actionFactory
                    actionToCopyURL:[[CrURL alloc] initWithGURL:nodeURL]]];
  // Add edit menu item.
  UIAction* editAction = [actionFactory actionToEditWithBlock:^{
    [weakSelf editBookmarkNodeWithID:nodeID];
  }];
  [menuElements addObject:editAction];
  // Add share menu item.
  [menuElements addObject:[actionFactory actionToShareWithBlock:^{
                  [weakSelf shareURLBookmarkNodeWithID:nodeID
                                             indexPath:indexPath];
                }]];
  // Add delete menu item.
  UIAction* deleteAction = [actionFactory actionToDeleteWithBlock:^{
    [weakSelf deleteBookmarkNodeWithID:nodeID
                            userAction:"MobileBookmarkManagerEntryDeleted"];
  }];
  [menuElements addObject:deleteAction];
  // Disable Edit and Delete if the node cannot be edited.
  if (!canEditNode) {
    editAction.attributes = UIMenuElementAttributesDisabled;
    deleteAction.attributes = UIMenuElementAttributesDisabled;
  }
  return [UIMenu menuWithTitle:@"" children:menuElements];
}

// Returns contextual menu for a folder node at `indexPath`.
- (UIMenu*)folderNodeContextualMenuWithIndexPath:(NSIndexPath*)indexPath
                                     canEditNode:(BOOL)canEditNode {
  const BookmarkNode* folderNode = [self nodeAtIndexPath:indexPath];
  const int64_t nodeID = folderNode->id();
  DCHECK_EQ(folderNode->type(), BookmarkNode::FOLDER);
  // Record that this context menu was shown to the user.
  RecordMenuShown(kMenuScenarioHistogramBookmarkFolder);
  ActionFactory* actionFactory = [[ActionFactory alloc]
      initWithScenario:kMenuScenarioHistogramBookmarkFolder];
  NSMutableArray<UIMenuElement*>* menuElements = [[NSMutableArray alloc] init];
  // Add edit menu item.
  __weak __typeof(self) weakSelf = self;
  UIAction* editAction = [actionFactory actionToEditWithBlock:^{
    [weakSelf editFolderNodeWithID:nodeID];
  }];
  [menuElements addObject:editAction];
  // Add move menu item.
  UIAction* moveAction = [actionFactory actionToMoveFolderWithBlock:^{
    [weakSelf moveBookmarkNodeWithIDs:{nodeID}
                           userAction:"MobileBookmarkManagerMoveToFolder"];
  }];
  [menuElements addObject:moveAction];
  // Disable Edit and Move if the node cannot be edited.
  if (!canEditNode) {
    editAction.attributes = UIMenuElementAttributesDisabled;
    moveAction.attributes = UIMenuElementAttributesDisabled;
  }
  return [UIMenu menuWithTitle:@"" children:menuElements];
}

// Opens the folder move editor for the given node IDs.
- (void)moveBookmarkNodeWithIDs:(const BookmarkNodeIDSet&)nodeIDs
                     userAction:(const char*)userAction {
  DCHECK(!_folderChooserCoordinator);
  DCHECK(nodeIDs.size() > 0);

  std::set<raw_ptr<const bookmarks::BookmarkNode>> nodes;
  for (int64_t nodeID : nodeIDs) {
    const BookmarkNode* node = [self findNodeByID:nodeID];
    if (node) {
      nodes.insert(node);
    }
  }

  if (nodes.empty()) {
    // While the contextual menu was opened, the nodes might have been removed.
    // If the nodes don't exist anymore, there nothing to do.
    return;
  }

  base::RecordAction(base::UserMetricsAction(userAction));
  const BookmarkNode* movedNode = *(nodes.begin());
  const BookmarkNode* selectedFolder = movedNode->parent();
  _UIDisabled = YES;
  _folderChooserCoordinator = [[BookmarksFolderChooserCoordinator alloc]
      initWithBaseViewController:self.navigationController
                         browser:_browser.get()
                      movedNodes:nodes];
  [_folderChooserCoordinator setSelectedFolder:selectedFolder];
  _folderChooserCoordinator.delegate = self;
  [_folderChooserCoordinator start];
}

// Deletes `nodeID` if it still exists and records `userAction`.
- (void)deleteBookmarkNodeWithID:(int64_t)nodeID
                      userAction:(const char*)userAction {
  const BookmarkNode* node = [self findNodeByID:nodeID];
  if (!node) {
    // While the contextual menu was opened, the nodes might have been removed.
    // If the nodes don't exist anymore, there nothing to do.
    return;
  }
  bookmark_utils_ios::NodeSet nodes = {node};
  [self deleteBookmarkNodes:nodes userAction:userAction];
}

// Deletes the `nodes` and records `userAction`.
- (void)deleteBookmarkNodes:(const bookmark_utils_ios::NodeSet&)nodes
                 userAction:(const char*)userAction {
  DCHECK_GE(nodes.size(), 1u);
  base::RecordAction(base::UserMetricsAction(userAction));
  [self.snackbarCommandsHandler
      showSnackbarMessage:bookmark_utils_ios::DeleteBookmarksWithUndoSnackbar(
                              nodes, _bookmarkModel.get(), self.profile,
                              FROM_HERE)];
  [self setTableViewEditing:NO];
}

// Opens the editor for `nodeID` node, if it still exists. The node has to be
// a bookmark node.
- (void)editBookmarkNodeWithID:(int64_t)nodeID {
  const BookmarkNode* bookmarkNode = [self findNodeByID:nodeID];
  if (!bookmarkNode) {
    // While the contextual menu was opened, the node might has been removed.
    // If the node doesn't exist anymore, there nothing to do.
    return;
  }
  DCHECK_EQ(bookmarkNode->type(), BookmarkNode::URL);
  base::RecordAction(
      base::UserMetricsAction("MobileBookmarkManagerEditBookmark"));
  [self stopBookmarksEditorCoordinator];
  self.bookmarkEditorCoordinator = [[BookmarksEditorCoordinator alloc]
      initWithBaseViewController:self.navigationController
                         browser:_browser.get()
                            node:bookmarkNode
         snackbarCommandsHandler:self.snackbarCommandsHandler];
  self.bookmarkEditorCoordinator.delegate = self;
  _UIDisabled = YES;
  [self.bookmarkEditorCoordinator start];
}

// Opens the editor for `nodeID` node, if it still exists. The node has to be
// a folder node.
- (void)editFolderNodeWithID:(int64_t)nodeID {
  const BookmarkNode* bookmarkNode = [self findNodeByID:nodeID];
  if (!bookmarkNode) {
    // While the contextual menu was opened, the node might has been removed.
    // If the node doesn't exist anymore, there nothing to do.
    return;
  }
  DCHECK_EQ(bookmarkNode->type(), BookmarkNode::FOLDER);
  base::RecordAction(
      base::UserMetricsAction("MobileBookmarkManagerEditFolder"));
  [self stopBookmarksFolderEditorCoordinator];
  self.folderEditorCoordinator = [[BookmarksFolderEditorCoordinator alloc]
      initWithBaseViewController:self.navigationController
                         browser:_browser.get()
                      folderNode:bookmarkNode];
  self.folderEditorCoordinator.delegate = self;
  [self.folderEditorCoordinator start];
  _UIDisabled = YES;
}

// Opens the urls. If `inIncognito` is YES but incognito is unavailable, or if
// `inIncognito` is NO but incognito is forced, do nothing.
- (void)openAllURLs:(std::vector<GURL>)urls
        inIncognito:(BOOL)inIncognito
             newTab:(BOOL)newTab {
  if (inIncognito) {
    if (![self isIncognitoAvailable]) {
      return;
    }
    SceneState* scene = _browser.get()->GetSceneState();
    if (scene.incognitoState.authenticationRequired) {
      __weak BookmarksHomeViewController* weakSelf = self;
      IncognitoReauthSceneAgent* reauthAgent =
          [IncognitoReauthSceneAgent agentFromScene:scene];
      [reauthAgent
          authenticateIncognitoContentWithCompletionBlock:^(BOOL success) {
            if (success) {
              [weakSelf openAllURLs:urls inIncognito:inIncognito newTab:newTab];
            }
          }];
      return;
    }
  } else if ([self isIncognitoForced]) {
    return;
  }

  [self cacheIndexPathRow];
  [self.homeDelegate bookmarkHomeViewControllerWantsDismissal:self
                                             navigationToUrls:urls
                                                  inIncognito:inIncognito
                                                       newTab:newTab];
}

- (void)showBatchUploadDialog:(CGRect)targetRect {
  if (self.actionSheetCoordinator) {
    return;
  }
  _UIDisabled = YES;
  __weak BookmarksHomeViewController* weakSelf = self;
  [self.mediator
      queryLocalBookmarks:^(int localBookmarksCount, std::string userEmail) {
        [weakSelf queryCallbackWithTargetRect:targetRect
                                        count:localBookmarksCount
                                        email:userEmail];
      }];
}

- (void)triggerBatchUploadFor:(int)localBookmarksCount
                    userEmail:(std::string)userEmail {
  [self dismissActionSheetCoordinator];
  [self.mediator triggerBatchUpload];

  base::UmaHistogramCounts100000(
      "IOS.Bookmarks.BulkSaveBookmarksInAccountCount", localBookmarksCount);

  [self refreshContents];
  _UIDisabled = NO;

  NSString* snackbarMessage = base::SysUTF16ToNSString(
      base::i18n::MessageFormatter::FormatWithNamedArgs(
          l10n_util::GetStringUTF16(
              IDS_IOS_BOOKMARKS_HOME_BULK_UPLOAD_SNACKBAR_MESSAGE),
          "count", localBookmarksCount, "email", userEmail));
  [self.snackbarCommandsHandler showSnackbarWithMessage:snackbarMessage
                                             buttonText:nil
                                          messageAction:nil
                                       completionAction:nil];
}

#pragma mark - Action Sheet Callback Helper

- (void)queryCallbackWithTargetRect:(CGRect)targetRect
                              count:(int)localBookmarksCount
                              email:(std::string)userEmail {
  NSString* alertTitle = l10n_util::GetPluralNSStringF(
      IDS_IOS_BOOKMARKS_HOME_BULK_UPLOAD_ALERT_TITLE, localBookmarksCount);
  NSString* alertDescription = base::SysUTF16ToNSString(
      base::i18n::MessageFormatter::FormatWithNamedArgs(
          l10n_util::GetStringUTF16(
              IDS_IOS_BOOKMARKS_HOME_BULK_UPLOAD_ALERT_DESCRIPTION),
          "count", localBookmarksCount, "email", userEmail));
  // queryLocalBookmarks() should execute the callback almost immediately.
  // This CHECK ensures that the action sheet coordinator is never opened
  // twice.
  CHECK(!self.actionSheetCoordinator);
  self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
      initWithBaseViewController:self
                         browser:self->_browser.get()
                           title:alertTitle
                         message:alertDescription
                            rect:targetRect
                            view:self.tableView];
  __weak __typeof(self) weakSelf = self;
  // Create the confirm button.
  [self.actionSheetCoordinator
      addItemWithTitle:GetNSString(
                           IDS_IOS_BOOKMARKS_HOME_BULK_UPLOAD_ALERT_BUTTON)
                action:^{
                  [weakSelf bulkUploadActionWithCount:localBookmarksCount
                                            userEmail:userEmail];
                }
                 style:UIAlertActionStyleDefault];

  // Create the cancel button.
  [self.actionSheetCoordinator
      addItemWithTitle:GetNSString(
                           IDS_IOS_BOOKMARKS_HOME_BULK_UPLOAD_ALERT_CANCEL)
                action:^{
                  [weakSelf bulkUploadCancel];
                }
                 style:UIAlertActionStyleCancel];

  // Show the alert.
  [self.actionSheetCoordinator start];
}

- (void)bulkUploadActionWithCount:(int)localBookmarksCount
                        userEmail:(std::string)userEmail {
  base::RecordAction(base::UserMetricsAction(
      "MobileBookmarksManagerBulkSaveBookmarksToAccountDialog"
      "Accepted"));
  [self triggerBatchUploadFor:localBookmarksCount
                    userEmail:std::move(userEmail)];
}

- (void)bulkUploadCancel {
  base::RecordAction(base::UserMetricsAction(
      "MobileBookmarksManagerBulkSaveBookmarksToAccountDialog"
      "Cancelled"));
  [self dismissActionSheetCoordinator];
}

#pragma mark - Navigation Bar Callbacks

- (void)navigationBarCancel:(id)sender {
  base::RecordAction(base::UserMetricsAction("MobileBookmarkManagerClose"));
  [self navigateAway];
  [self dismissWithURL:GURL()];
}

#pragma mark - More Private Methods

- (void)handleSelectUrlForNavigation:(const GURL&)url {
  [self dismissWithURL:url];
}

- (void)handleSelectFolderForNavigation:(const BookmarkNode*)folder {
  // The navigation controller can be nil if `self`’s view is being dismissed.
  if (self.navigationController &&
      !self.mediator.currentlyShowingSearchResults) {
    BookmarksHomeViewController* controller =
        [self createControllerWithDisplayedFolderNode:folder];
    [self.navigationController pushViewController:controller animated:YES];
    if (![self.navigationController.viewControllers
            containsObject:controller]) {
      // This push can fail if the navigation controller’s list of VC is
      // currently changing. See crbug.com/515121885.
      [controller shutdown];
    }
    return;
  }
  [self jumpToFolder:folder];
}

- (void)jumpToFolder:(const BookmarkNode*)folder {
  // Clear bookmark path cache.
  if (_isBeingDismissed) {
    // The navigation controller is being dismissed.
    // Do not open more views.
    return;
  }
  int64_t unusedFolderId;
  int unusedIndexPathRow;
  PrefService* prefService = self.profile->GetPrefs();
  while ([BookmarkPathCache
      bookmarkTopMostRowCacheWithPrefService:prefService
                               bookmarkModel:_bookmarkModel.get()
                                    folderId:&unusedFolderId
                                  topMostRow:&unusedIndexPathRow]) {
    [BookmarkPathCache clearBookmarkTopMostRowCacheWithPrefService:prefService];
  }

  // Rebuild folder controller list, going back up the tree.
  NSMutableArray<BookmarksHomeViewController*>* stack = [NSMutableArray array];
  std::vector<const BookmarkNode*> nodes;
  const BookmarkNode* cursor = folder;
  while (cursor) {
    // Build reversed list of nodes to restore bookmark path below.
    nodes.insert(nodes.begin(), cursor);

    // Build reversed list of controllers.
    BookmarksHomeViewController* controller =
        [self createControllerWithDisplayedFolderNode:cursor];
    [stack insertObject:controller atIndex:0];

    // Setup now, so that the back button labels shows parent folder
    // title and that we don't show large title everywhere.
    [self setupNavigationForBookmarksHomeViewController:controller
                                      usingBookmarkNode:cursor];

    cursor = cursor->parent();
  }

  // Reconstruct bookmark path cache.
  for (const BookmarkNode* node : nodes) {
    [BookmarkPathCache
        cacheBookmarkTopMostRowWithPrefService:prefService
                                      folderId:node->id()
                                     inStorage:bookmark_utils_ios::
                                                   GetBookmarkStorageType(
                                                       node,
                                                       _bookmarkModel.get())
                                    topMostRow:0];
  }

  [self navigateAway];

  // At root, since there's a large title, the search bar is lower than on
  // whatever destination folder it is transitioning to (root is never
  // reachable through search). To avoid a kink in the animation, the title
  // is set to regular size, which means the search bar is at same level at
  // beginning and end of animation. This controller will be replaced in
  // `stack` so there's no need to care about restoring this.
  if ([self isDisplayingBookmarkRoot]) {
    self.navigationItem.largeTitleDisplayMode =
        UINavigationItemLargeTitleDisplayModeNever;
  }

  __weak BookmarksHomeViewController* weakSelf = self;
  auto completion = ^{
    [weakSelf searchControllerDismissCallbackWithStack:stack];
  };
  NSArray<__kindof UIViewController*>* previousStack =
      self.navigationController.viewControllers;
  for (UIViewController* controller in previousStack) {
    BookmarksHomeViewController* bookmarksHomeViewController =
        base::apple::ObjCCastStrict<BookmarksHomeViewController>(controller);
    bookmarksHomeViewController.displayedFolderNode = nullptr;
  }

  [self.searchController dismissViewControllerAnimated:YES
                                            completion:completion];
}

- (void)searchControllerDismissCallbackWithStack:
    (NSMutableArray<BookmarksHomeViewController*>*)stack {
  NSArray<__kindof UIViewController*>* previousStack =
      self.navigationController.viewControllers;
  [self.navigationController setViewControllers:stack animated:YES];
  for (UIViewController* controller in previousStack) {
    BookmarksHomeViewController* bookmarksHomeViewController =
        base::apple::ObjCCastStrict<BookmarksHomeViewController>(controller);
    [bookmarksHomeViewController shutdown];
  }
}

- (void)handleSelectEditNodes:(const std::set<const BookmarkNode*>&)nodes {
  // Early return if bookmarks table is not in edit mode.
  if (!self.mediator.currentlyInEditMode) {
    return;
  }

  if (nodes.empty()) {
    // if nothing to select, exit edit mode.
    if (![self hasBookmarksOrFolders]) {
      [self setTableViewEditing:NO];
      return;
    }
    [self setContextBarState:BookmarksContextBarBeginSelection];
    return;
  }

  if (nodes.size() == 1) {
    const BookmarkNode* node = *nodes.begin();
    if (node->is_url()) {
      [self setContextBarState:BookmarksContextBarSingleURLSelection];
    } else {
      CHECK_EQ(node->type(), BookmarkNode::FOLDER, base::NotFatalUntil::M152);
      [self setContextBarState:BookmarksContextBarSingleFolderSelection];
    }
    return;
  }

  BOOL foundURL = NO;
  BOOL foundFolder = NO;
  for (const BookmarkNode* node : nodes) {
    if (!foundURL && node->is_url()) {
      foundURL = YES;
    } else if (!foundFolder && node->is_folder()) {
      foundFolder = YES;
    }
    // Break early, if we found both types of nodes.
    if (foundURL && foundFolder) {
      break;
    }
  }

  // Only URLs are selected.
  if (foundURL && !foundFolder) {
    [self setContextBarState:BookmarksContextBarMultipleURLSelection];
    return;
  }
  // Only Folders are selected.
  if (!foundURL && foundFolder) {
    [self setContextBarState:BookmarksContextBarMultipleFolderSelection];
    return;
  }
  // Mixed selection.
  if (foundURL && foundFolder) {
    [self setContextBarState:BookmarksContextBarMixedSelection];
    return;
  }

  NOTREACHED(base::NotFatalUntil::M152);
}

- (void)handleRefreshContextBar {
  // At default state, the enable state of context bar buttons could change
  // during refresh.
  if (self.contextBarState == BookmarksContextBarDefault) {
    [self setBookmarksContextBarButtonsDefaultState];
  }
}

- (BOOL)isAtTopOfNavigation {
  return (self.navigationController.topViewController == self);
}

// Stop edit of current bookmark folder name, if any.
- (void)stopEdittingBookmarkOnTraitChange {
  [self.editingFolderCell stopEdit];
}

#pragma mark - BookmarkTableCellTitleEditDelegate

- (void)textDidChangeTo:(NSString*)newName {
  if (!_bookmarkModel) {
    return;
  }
  CHECK(self.mediator.editingFolderNode, base::NotFatalUntil::M152);
  self.mediator.addingNewFolder = NO;
  if (newName.length > 0) {
    _bookmarkModel->SetTitle(self.mediator.editingFolderNode,
                             base::SysNSStringToUTF16(newName),
                             bookmarks::metrics::BookmarkEditSource::kUser);
  }
  self.mediator.editingFolderNode = nullptr;
  self.editingFolderCell = nil;
  [self refreshContents];
}

#pragma mark - BookmarksFolderChooserCoordinatorDelegate

- (void)bookmarksFolderChooserCoordinatorDidConfirm:
            (BookmarksFolderChooserCoordinator*)coordinator
                                 withSelectedFolder:
                                     (const BookmarkNode*)folder {
  CHECK(_folderChooserCoordinator, base::NotFatalUntil::M152);
  CHECK(folder, base::NotFatalUntil::M152);
  CHECK(!folder->is_url(), base::NotFatalUntil::M152);

  // Copy the list of moved nodes from BookmarksFolderChooserCoordinator before
  // `stopFolderChooserCoordinator` sets `_folderChooserCoordinator` to nil.
  std::set<raw_ptr<const bookmarks::BookmarkNode>> editedNodesSet =
      _folderChooserCoordinator.movedNodes;
  [self stopFolderChooserCoordinator];

  [self setTableViewEditing:NO];
  if (editedNodesSet.empty()) {
    // All nodes of the sets have been deleted in the meantime. Nothing to do.
    return;
  }
  ProfileIOS* profile = self.profile;
  std::vector<const BookmarkNode*> editedNodesVector(editedNodesSet.begin(),
                                                     editedNodesSet.end());
  SnackbarMessage* snackbarMessage =
      bookmark_utils_ios::MoveBookmarksWithUndoSnackbar(
          editedNodesVector, _bookmarkModel.get(), folder, profile,
          AuthenticationServiceFactory::GetForProfile(profile)->GetWeakPtr(),
          SyncServiceFactory::GetForProfile(profile));
  if (snackbarMessage) {
    [self.snackbarCommandsHandler showSnackbarMessage:snackbarMessage];
  }
}

- (void)bookmarksFolderChooserCoordinatorDidCancel:
    (BookmarksFolderChooserCoordinator*)coordinator {
  CHECK(_folderChooserCoordinator, base::NotFatalUntil::M152);
  [self stopFolderChooserCoordinator];
  [self setTableViewEditing:NO];
}

#pragma mark - BookmarksEditorCoordinatorDelegate

- (void)bookmarksEditorCoordinatorShouldStop:
    (BookmarksEditorCoordinator*)coordinator {
  CHECK_EQ(coordinator, self.bookmarkEditorCoordinator);
  [self stopBookmarksEditorCoordinator];
}

- (void)bookmarkEditorWillCommitTitleOrURLChange:
    (BookmarksEditorCoordinator*)coordinator {
  CHECK_EQ(coordinator, self.bookmarkEditorCoordinator);
  [self setTableViewEditing:NO];
}

#pragma mark - BookmarksFolderEditorCoordinatorDelegate

- (void)bookmarksFolderEditorCoordinator:
            (BookmarksFolderEditorCoordinator*)folderEditor
              didFinishEditingFolderNode:
                  (const bookmarks::BookmarkNode*)folder {
  CHECK_EQ(folderEditor, self.folderEditorCoordinator);
  CHECK(folder, base::NotFatalUntil::M152);
  [self stopBookmarksFolderEditorCoordinator];
}

- (void)bookmarksFolderEditorCoordinatorShouldStop:
    (BookmarksFolderEditorCoordinator*)coordinator {
  CHECK_EQ(coordinator, self.folderEditorCoordinator);
  [self stopBookmarksFolderEditorCoordinator];
}

- (void)bookmarksFolderEditorWillCommitTitleChange:
    (BookmarksFolderEditorCoordinator*)coordinator {
  CHECK_EQ(coordinator, self.folderEditorCoordinator);
  [self setTableViewEditing:NO];
}

#pragma mark - BookmarkModelBridgeObserver

- (void)bookmarkModelLoaded {
  CHECK(!self.displayedFolderNode, base::NotFatalUntil::M152);
  self.displayedFolderNode = _bookmarkModel->root_node();

  // If the view hasn't loaded yet, then return early. The eventual call to
  // viewDidLoad will properly initialize the views.  This early return must
  // come *after* setting displayedFolderNode above.
  if (![self isViewLoaded]) {
    return;
  }

  int64_t unusedFolderId;
  int unusedIndexPathRow;
  // Bookmark Model is loaded after presenting Bookmarks,  we need to check
  // again here if restoring of cache position is needed.  It is to prevent
  // crbug.com/765503.
  if ([BookmarkPathCache
          bookmarkTopMostRowCacheWithPrefService:self.profile->GetPrefs()
                                   bookmarkModel:_bookmarkModel.get()
                                        folderId:&unusedFolderId
                                      topMostRow:&unusedIndexPathRow]) {
    self.isReconstructingFromCache = YES;
  }

  CHECK(self.spinnerView, base::NotFatalUntil::M152);
  __weak BookmarksHomeViewController* weakSelf = self;
  [self.spinnerView stopWaitingWithCompletion:^{
    [weakSelf spinnerCompletion];
  }];
}

- (void)didChangeNode:(const BookmarkNode*)bookmarkNode {
  // No-op here.  Bookmarks might be refreshed in BookmarksHomeMediator.
}

- (void)didChangeChildrenForNode:(const BookmarkNode*)bookmarkNode {
  // No-op here.  Bookmarks might be refreshed in BookmarksHomeMediator.
}

- (void)didMoveNode:(const BookmarkNode*)bookmarkNode
         fromParent:(const BookmarkNode*)oldParent
           toParent:(const BookmarkNode*)newParent {
  // No-op here.  Bookmarks might be refreshed in BookmarksHomeMediator.
}

- (void)didDeleteNode:(const BookmarkNode*)node
           fromFolder:(const BookmarkNode*)folder {
  if (self.displayedFolderNode == node) {
    [self setTableViewEditing:NO];
  }
}

- (void)bookmarkModelRemovedAllNodes {
  // No-op
}

#pragma mark - BookmarkModelBridgeObserver helper

- (void)spinnerCompletion {
  __weak __typeof(self) weakSelf = self;
  [UIView animateWithDuration:0.2f
      animations:^{
        weakSelf.spinnerView.alpha = 0.0;
      }
      completion:^(BOOL finished) {
        [weakSelf spinnerCompletionCompletion];
      }];
  [self loadBookmarkViews];
  [self.tableView reloadData];
}

- (void)spinnerCompletionCompletion {
  // By the time completion block is called, the backgroundView could be
  // another view, like the empty view background. Only clear the
  // background if it is still the spinner.
  if (self.tableView.backgroundView == self.spinnerView) {
    self.tableView.backgroundView = nil;
  }
  self.spinnerView = nil;
}

#pragma mark - UIAccessibilityAction

- (BOOL)accessibilityPerformEscape {
  if ([self isDisplayingBookmarkRoot]) {
    [self navigationBarCancel:self];
  } else {
    [self backAction];
  }
  return YES;
}

#pragma mark - Accessors & Mutators

- (UIBarButtonItem*)deleteButton {
  if (!_deleteButton) {
    _deleteButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemTrash
                             target:self
                             action:@selector(leadingButtonClicked)];
    _deleteButton.accessibilityLabel =
        GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_DELETE);
    _deleteButton.tintColor = [UIColor colorNamed:kRedColor];
    _deleteButton.accessibilityIdentifier =
        kBookmarksHomeLeadingButtonIdentifier;
  }
  return _deleteButton;
}

- (UIBarButtonItem*)moreButton {
  if (!_moreButton) {
    NSString* titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_MORE);
    _moreButton =
        [[UIBarButtonItem alloc] initWithTitle:titleString
                                         style:UIBarButtonItemStylePlain
                                        target:self
                                        action:@selector(centerButtonClicked)];
    _moreButton.accessibilityIdentifier = kBookmarksHomeCenterButtonIdentifier;
  }
  return _moreButton;
}

- (void)setContextBarState:(BookmarksContextBarState)state {
  _contextBarState = state;
  switch (state) {
    case BookmarksContextBarDefault:
      [self setBookmarksContextBarButtonsDefaultState];
      [self setBookmarksNavigationBarButtonsDefaultState];
      break;
    case BookmarksContextBarBeginSelection:
      [self setBookmarksContextBarSelectionStartState];
      [self setBookmarksNavigationBarSelectionState];
      self.moreButton.enabled = NO;
      self.deleteButton.enabled = NO;
      break;
    case BookmarksContextBarSingleURLSelection:
    case BookmarksContextBarMultipleURLSelection:
    case BookmarksContextBarMultipleFolderSelection:
    case BookmarksContextBarMixedSelection:
    case BookmarksContextBarSingleFolderSelection:
      // Reset to start state, and then override with customizations that apply.
      [self setBookmarksContextBarSelectionStartState];
      [self setBookmarksNavigationBarSelectionState];
      self.moreButton.enabled = YES;
      self.deleteButton.enabled = YES;
      break;
    case BookmarksContextBarNone:
    default:
      break;
  }
}

- (void)setExternalBookmark:(const BookmarkNode*)node {
  _externalBookmark = node;
}

#pragma mark - Private

// Called when a favicon is fetched.
- (void)didFetchFaviconAttributes:(FaviconAttributes*)attributes
                           cached:(bool)cached
                             item:(BookmarksHomeNodeItem*)item
                        indexPath:(NSIndexPath*)indexPath
                           nodeId:(int64_t)nodeId {
  const BookmarkNode* node = [self hasItemAtIndexPath:indexPath]
                                 ? [self nodeAtIndexPath:indexPath]
                                 : nil;
  // Due to search filtering, we also need to validate the indexPath
  // requested versus what is in the table now.
  if (!node || node->id() != nodeId) {
    return;
  }
  item.faviconAttributes = attributes;
  if (!cached && attributes.faviconImage) {
    if ([self.tableViewModel itemAtIndexPath:indexPath] != item) {
      return;
    }
    LegacyTableViewCell* cell =
        base::apple::ObjCCastStrict<LegacyTableViewCell>(
            [self.tableView cellForRowAtIndexPath:indexPath]);
    if (!cell) {
      return;
    }
    // Even if Apple documentation hints toward reconfiguring the row instead
    // of just updating the cell, it creates a visible jank. Use the item
    // configuration method instead. See crbug.com/479692041 for more info.
    [item configureCell:cell];
  }
}

// Creates a delete action for the swipe menu with destructive style (and red
// color).
- (UIContextualAction*)createDeleteActionForIndexPath:(NSIndexPath*)indexPath {
  __weak __typeof(self) weakSelf = self;

  return [UIContextualAction
      contextualActionWithStyle:UIContextualActionStyleDestructive
                          title:
                              GetNSString(
                                  IDS_IOS_REMINDER_NOTIFICATIONS_SWIPE_ACTION_DELETE)
                        handler:^(UIContextualAction* action,
                                  UIView* sourceView,
                                  void (^completionHandler)(BOOL)) {
                          [weakSelf
                              handleDeleteActionForIndexPath:indexPath
                                           completionHandler:completionHandler];
                        }];
}

// Creates a remind action for the swipe menu with normal style (and orange
// color).
- (UIContextualAction*)createRemindActionForIndexPath:(NSIndexPath*)indexPath {
  __weak __typeof(self) weakSelf = self;

  UIContextualAction* remindAction = [UIContextualAction
      contextualActionWithStyle:UIContextualActionStyleNormal
                          title:
                              GetNSString(
                                  IDS_IOS_REMINDER_NOTIFICATIONS_SWIPE_ACTION_REMIND)
                        handler:^(UIContextualAction* action,
                                  UIView* sourceView,
                                  void (^completionHandler)(BOOL)) {
                          [weakSelf
                              handleRemindActionForIndexPath:indexPath
                                           completionHandler:completionHandler];
                        }];

  remindAction.backgroundColor = [UIColor colorNamed:kOrange500Color];

  return remindAction;
}

// Handles the delete action for a cell at the given `indexPath` and calls the
// `completionHandler` when complete. This is used by swipe actions.
- (void)handleDeleteActionForIndexPath:(NSIndexPath*)indexPath
                     completionHandler:(void (^)(BOOL))completionHandler {
  const BookmarkNode* bookmarkNode = [self nodeAtIndexPath:indexPath];

  BOOL canDeleteNode = bookmarkNode &&
                       [self isNodeEditableByUser:bookmarkNode] &&
                       [self isEditBookmarksEnabled];

  if (!canDeleteNode) {
    if (completionHandler) {
      completionHandler(NO);
    }
    return;
  }

  [self deleteBookmarkNodeWithID:bookmarkNode->id()
                      userAction:"MobileBookmarkManagerEntryDeleted"];

  if (completionHandler) {
    completionHandler(YES);
  }
}

// Handles the remind action for a cell at the given `indexPath`, displays the
// tab reminder UI for the selected item, and calls the `completionHandler` when
// complete. This is used by swipe actions when Tab Reminders are enabled.
- (void)handleRemindActionForIndexPath:(NSIndexPath*)indexPath
                     completionHandler:(void (^)(BOOL))completionHandler {
  const BookmarkNode* bookmarkNode = [self nodeAtIndexPath:indexPath];

  if (!bookmarkNode) {
    if (completionHandler) {
      completionHandler(NO);
    }
    return;
  }

  [self.homeDelegate bookmarkHomeViewController:self
             wantsToShowSetTabReminderUIForNode:bookmarkNode];

  if (completionHandler) {
    completionHandler(YES);
  }
}

- (void)stopSigninCoordinator {
  [_signinCoordinator stop];
  _signinCoordinator = nil;
}

// Returns the profile.
- (ProfileIOS*)profile {
  if (Browser* browser = _browser.get()) {
    return browser->GetProfile()->GetOriginalProfile();
  }
  return nullptr;
}

- (void)dismissActionSheetCoordinator {
  _UIDisabled = NO;
  [self.actionSheetCoordinator stop];
  self.actionSheetCoordinator = nil;
}

// Stops the folder chooser coordinator.
- (void)stopFolderChooserCoordinator {
  [_folderChooserCoordinator stop];
  _UIDisabled = NO;
  _folderChooserCoordinator.delegate = nil;
  _folderChooserCoordinator = nil;
}

// Stops the bookmark editor coordinator.
- (void)stopBookmarksEditorCoordinator {
  _UIDisabled = NO;
  [self.bookmarkEditorCoordinator stop];
  self.bookmarkEditorCoordinator.delegate = nil;
  self.bookmarkEditorCoordinator = nil;
}

// Stops the folder editor coordinator.
- (void)stopBookmarksFolderEditorCoordinator {
  _UIDisabled = NO;
  [self.folderEditorCoordinator stop];
  self.folderEditorCoordinator.delegate = nil;
  self.folderEditorCoordinator = nil;
}

- (BOOL)isDisplayingBookmarkRoot {
  return self.displayedFolderNode == _bookmarkModel->root_node();
}

// Check if any of our controller is presenting. We don't consider when this
// controller is presenting the search controller.
// Note that when adding a controller that can present, it should be added in
// context here.
- (BOOL)isAnyControllerPresenting {
  return (([self presentedViewController] &&
           [self presentedViewController] != self.searchController) ||
          [self.searchController presentedViewController] ||
          [self.navigationController presentedViewController]);
}

- (void)setupUIStackCacheIfApplicable {
  self.isReconstructingFromCache = NO;

  NSArray<BookmarksHomeViewController*>* replacementViewControllers =
      [self cachedViewControllerStack];
  CHECK(replacementViewControllers, base::NotFatalUntil::M152);
  [self.navigationController setViewControllers:replacementViewControllers];
}

// Set up context bar for the new UI.
- (void)setupContextBar {
  if (_isShutDown) {
    return;
  }
  if (![self isDisplayingBookmarkRoot] ||
      self.mediator.currentlyShowingSearchResults) {
    self.navigationController.toolbarHidden = NO;
    [self setContextBarState:BookmarksContextBarDefault];
  } else {
    self.navigationController.toolbarHidden = YES;
  }
}

// Set up navigation bar for `viewController`'s navigationBar using `node`.
- (void)setupNavigationForBookmarksHomeViewController:
            (BookmarksHomeViewController*)viewController
                                    usingBookmarkNode:
                                        (const BookmarkNode*)node {
  // Disable large titles on every VC but the root controller.
  if (node != _bookmarkModel->root_node()) {
    viewController.navigationItem.largeTitleDisplayMode =
        UINavigationItemLargeTitleDisplayModeNever;
  }

  // Add custom title.
  viewController.title = bookmark_utils_ios::TitleForBookmarkNode(node);
  [viewController setBookmarksNavigationBarButtonsDefaultState];
}

// Back button callback for the new ui.
- (void)backAction {
  [self navigateAway];
  [self.navigationController popViewControllerAnimated:YES];
}

// Returns a button for the context bar that enables edit mode for bookmarks.
- (UIBarButtonItem*)createEditButton {
  NSString* titleString = GetNSString(IDS_IOS_SELECT_ACTION_TITLE);

  UIBarButtonItem* editButton =
      [[UIBarButtonItem alloc] initWithTitle:titleString
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:@selector(trailingButtonClicked)];
  editButton.accessibilityIdentifier = kBookmarksHomeTrailingButtonIdentifier;
  // The edit button is only enabled if the displayed root folder is editable
  // and has items. Note that Bookmarks Bar, Mobile Bookmarks, and Other
  // Bookmarks return as "editable" since their contents can be edited.
  // Editing bookmarks must also be allowed.
  editButton.enabled = [self isEditBookmarksEnabled] &&
                       [self hasBookmarksOrFolders] &&
                       [self isNodeEditableByUser:self.mediator.displayedNode];
  return editButton;
}

// Returns a button to add a new folder to the bookmarks.
- (UIBarButtonItem*)createNewFolderButton {
  UIImage* newFolderIcon = SymbolWithConfiguration(SymbolFolderBadgePlus, nil);

  UIBarButtonItem* newFolderButton =
      [[UIBarButtonItem alloc] initWithImage:newFolderIcon
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:@selector(leadingButtonClicked)];
  newFolderButton.accessibilityLabel =
      GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_NEW_FOLDER);
  newFolderButton.accessibilityIdentifier =
      kBookmarksHomeLeadingButtonIdentifier;
  newFolderButton.enabled = [self allowsNewFolder];
  return newFolderButton;
}

// Returns a button to select/deselect all bookmark nodes.
- (UIBarButtonItem*)createMultiSelectButton {
  CHECK(self.mediator.currentlyInEditMode);

  BOOL hasSelectedNodes = !self.mediator.selectedNodesForEditMode.empty();
  NSString* titleText = GetNSString(
      hasSelectedNodes ? IDS_IOS_BOOKMARK_NAVIGATION_BAR_DESELECT_ALL
                       : IDS_IOS_BOOKMARK_NAVIGATION_BAR_SELECT_ALL);
  NSString* accessibilityID =
      hasSelectedNodes ? kBookmarksHomeNavigationBarDeselectAllButtonIdentifier
                       : kBookmarksHomeNavigationBarSelectAllButtonIdentifier;
  SEL action = hasSelectedNodes ? @selector(didTapDeselectAll)
                                : @selector(didTapSelectAll);

  UIBarButtonItem* multiSelectButton =
      [[UIBarButtonItem alloc] initWithTitle:titleText
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:action];
  multiSelectButton.accessibilityIdentifier = accessibilityID;
  return multiSelectButton;
}

// Selects all editable bookmark nodes currently displayed.
- (void)didTapSelectAll {
  if (!_bookmarkModel) {
    return;
  }
  std::set<const bookmarks::BookmarkNode*> allEditableNodes;
  NSArray<TableViewItem*>* items = [self.tableViewModel
      itemsInSectionWithIdentifier:BookmarksHomeSectionIdentifierBookmarks];

  for (TableViewItem* item in items) {
    if (item.type == BookmarksHomeItemTypeBookmark) {
      BookmarksHomeNodeItem* nodeItem =
          base::apple::ObjCCastStrict<BookmarksHomeNodeItem>(item);
      const bookmarks::BookmarkNode* node =
          [nodeItem bookmarkNode:_bookmarkModel.get()];
      if (node && [self isNodeEditableByUser:node]) {
        allEditableNodes.insert(node);
      }
    }
  }

  self.mediator.selectedNodesForEditMode = allEditableNodes;
  [self restoreRowSelection];
  [self handleSelectEditNodes:self.mediator.selectedNodesForEditMode];
}

// Deselects all currently selected bookmark nodes.
- (void)didTapDeselectAll {
  // Deselect all rows in the table view.
  NSArray<NSIndexPath*>* selectedIndexPaths =
      [self.tableView indexPathsForSelectedRows];
  if (selectedIndexPaths.count > 0) {
    for (NSIndexPath* indexPath in selectedIndexPaths) {
      [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
    }
  }

  // Clear the selected nodes in the mediator.
  self.mediator.selectedNodesForEditMode.clear();
  [self handleSelectEditNodes:self.mediator.selectedNodesForEditMode];
}

// Returns a done button for the context bar that disables edit mode in
// bookmarks.
- (UIBarButtonItem*)createDoneButton {
  NSString* titleString = GetNSString(IDS_DONE);
  UIBarButtonItem* doneButton =
      [[UIBarButtonItem alloc] initWithTitle:titleString
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:@selector(trailingButtonClicked)];
  doneButton.accessibilityIdentifier = kBookmarksHomeTrailingButtonIdentifier;
  return doneButton;
}

// Returns a spacer to separate interactable buttons.
- (UIBarButtonItem*)createSpacerButton {
  return [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                           target:nil
                           action:nil];
}

// Returns a Done button for the right side of the navigation bar.
- (UIBarButtonItem*)createNavigationBarDoneButton {
  BOOL isEditingWithoutActiveSearch =
      self.mediator.currentlyInEditMode &&
      !self.mediator.currentlyShowingSearchResults;
  UIBarButtonSystemItem buttonItem = isEditingWithoutActiveSearch
                                         ? UIBarButtonSystemItemDone
                                         : UIBarButtonSystemItemClose;

  UIBarButtonItem* doneButton = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:buttonItem
                           target:self
                           action:@selector(didTapNavigationBarDoneButton)];
  doneButton.accessibilityIdentifier =
      kBookmarksHomeNavigationBarDoneButtonIdentifier;
  return doneButton;
}

// Called when the right navigation bar button is tapped. Exits search/edit mode
// or dismisses the view.
- (void)didTapNavigationBarDoneButton {
  BOOL isEditingWithoutActiveSearch =
      self.mediator.currentlyInEditMode &&
      !self.mediator.currentlyShowingSearchResults;

  if (isEditingWithoutActiveSearch) {
    [self setTableViewEditing:NO];
    return;
  }
  [self navigationBarCancel:nil];
}

// Saves the current position and asks the delegate to open the url, if delegate
// is set, otherwise opens the URL using URL loading service.
- (void)dismissWithURL:(const GURL&)url {
  [self cacheIndexPathRow];
  if (self.homeDelegate) {
    std::vector<GURL> urls;
    if (url.is_valid()) {
      urls.push_back(url);
    }
    [self.homeDelegate bookmarkHomeViewControllerWantsDismissal:self
                                               navigationToUrls:urls];
  } else {
    // Before passing the URL to the block, make sure the block has a copy of
    // the URL and not just a reference.
    const GURL localUrl(url);
    __weak BookmarksHomeViewController* weakSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
      [weakSelf loadURL:localUrl];
    });
  }
}

- (void)loadURL:(const GURL&)url {
  if (url.is_empty() || url.SchemeIs(url::kJavaScriptScheme)) {
    return;
  }

  bool is_ntp = self.webStateList->GetActiveWebState()->GetVisibleURL() ==
                kChromeUINewTabURL;
  new_tab_page_uma::RecordNTPAction(self.profile->IsOffTheRecord(), is_ntp,
                                    new_tab_page_uma::ACTION_OPENED_BOOKMARK);
  base::RecordAction(
      base::UserMetricsAction("MobileBookmarkManagerEntryOpened"));

  UrlLoadParams params = UrlLoadParams::InCurrentTab(url);
  params.web_params.transition_type = ui::PAGE_TRANSITION_AUTO_BOOKMARK;
  UrlLoadingBrowserAgent::FromBrowser(_browser.get())->Load(params);
}

- (void)addNewFolder {
  [self.editingFolderCell stopEdit];
  if (!self.mediator.displayedNode) {
    return;
  }
  self.mediator.addingNewFolder = YES;
  std::u16string folderTitle =
      l10n_util::GetStringUTF16(IDS_IOS_BOOKMARK_NEW_GROUP_DEFAULT_NAME);
  self.mediator.editingFolderNode = _bookmarkModel->AddFolder(
      self.mediator.displayedNode,
      self.mediator.displayedNode->children().size(), folderTitle);

  BOOL shouldDisplayCloudSlashIcon = [self.mediator
      shouldDisplayCloudSlashIconWithBookmarkNode:self.mediator.displayedNode];
  BookmarksHomeNodeItem* nodeItem =
      [BookmarksHomeNodeItem makeItemWithType:BookmarksHomeItemTypeBookmark
                                 bookmarkNode:self.mediator.editingFolderNode
                  shouldDisplayCloudSlashIcon:shouldDisplayCloudSlashIcon];
  [self.tableViewModel addItem:nodeItem
       toSectionWithIdentifier:BookmarksHomeSectionIdentifierBookmarks];

  // Insert the new folder cell at the end of the table.
  NSIndexPath* newRowIndexPath =
      [self.tableViewModel indexPathForItem:nodeItem];
  NSMutableArray* newRowIndexPaths =
      [[NSMutableArray alloc] initWithObjects:newRowIndexPath, nil];
  [self.tableView beginUpdates];
  [self.tableView insertRowsAtIndexPaths:newRowIndexPaths
                        withRowAnimation:UITableViewRowAnimationNone];
  [self.tableView endUpdates];

  // Scroll to the end of the table
  [self.tableView scrollToRowAtIndexPath:newRowIndexPath
                        atScrollPosition:UITableViewScrollPositionBottom
                                animated:YES];
}

- (BookmarksHomeViewController*)createControllerWithDisplayedFolderNode:
    (const BookmarkNode*)displayedFolderNode {
  BookmarksHomeViewController* controller =
      [[BookmarksHomeViewController alloc] initWithBrowser:_browser.get()];
  controller.displayedFolderNode = displayedFolderNode;
  controller.homeDelegate = self.homeDelegate;
  controller.sceneHandler = self.sceneHandler;
  controller.snackbarCommandsHandler = self.snackbarCommandsHandler;

  return controller;
}

// Row selection of the tableView will be cleared after reloadData.  This
// function is used to restore the row selection.  It also updates
// selectedNodesForEditMode in case some selected nodes are removed.
- (void)restoreRowSelection {
  if (!_bookmarkModel) {
    return;
  }
  // Create a new selectedNodesForEditMode set to check if some selected nodes
  // are removed.
  std::set<const BookmarkNode*> newEditNodes;

  // Add selected nodes to selectedNodesForEditMode only if they are not removed
  // (still exist in the table).
  NSArray<TableViewItem*>* items = [self.tableViewModel
      itemsInSectionWithIdentifier:BookmarksHomeSectionIdentifierBookmarks];
  for (TableViewItem* item in items) {
    BookmarksHomeNodeItem* nodeItem =
        base::apple::ObjCCastStrict<BookmarksHomeNodeItem>(item);
    const BookmarkNode* node = [nodeItem bookmarkNode:_bookmarkModel.get()];
    if (node && self.mediator.selectedNodesForEditMode.contains(node)) {
      newEditNodes.insert(node);
      // Reselect the row of this node.
      NSIndexPath* itemPath = [self.tableViewModel indexPathForItem:nodeItem];
      [self.tableView selectRowAtIndexPath:itemPath
                                  animated:NO
                            scrollPosition:UITableViewScrollPositionNone];
    }
  }

  // if selectedNodesForEditMode is changed, update it.
  if (self.mediator.selectedNodesForEditMode.size() != newEditNodes.size()) {
    self.mediator.selectedNodesForEditMode = newEditNodes;
    [self handleSelectEditNodes:self.mediator.selectedNodesForEditMode];
  }
}

- (BOOL)allowsNewFolder {
  // When the current root node has been removed remotely (becomes NULL),
  // or when displaying search results, creating new folder is forbidden.
  // The root folder displayed by the table view must also be editable to allow
  // creation of new folders. Note that Bookmarks Bar, Mobile Bookmarks, and
  // Other Bookmarks return as "editable" since the user can edit the contents
  // of those folders. Editing bookmarks must also be allowed.
  return self.mediator.displayedNode != NULL &&
         !self.mediator.currentlyShowingSearchResults &&
         [self isEditBookmarksEnabled] &&
         [self isNodeEditableByUser:self.mediator.displayedNode];
}

- (int)topMostVisibleIndexPathRow {
  // If on root node screen, return 0.
  if (_bookmarkModel && [self isDisplayingBookmarkRoot]) {
    return 0;
  }

  UITableView* tableView = self.tableView;

  // If no rows in table, return 0.
  NSArray* visibleIndexPaths = [tableView indexPathsForVisibleRows];
  if (!visibleIndexPaths.count) {
    return 0;
  }

  NSIndexPath* topMostIndexPath;
  UIView* navigationBar = self.navigationController.navigationBar;
  CGRect navigationBarFrame = [navigationBar.superview
      convertRect:self.navigationController.navigationBar.frame
           toView:nil];
  // Take the first row that has its center visible below the navigation bar.
  for (NSIndexPath* indexPath in visibleIndexPaths) {
    CGRect rowFrame =
        [tableView convertRect:[tableView rectForRowAtIndexPath:indexPath]
                        toView:nil];
    if (CGRectGetMidY(rowFrame) > CGRectGetMaxY(navigationBarFrame)) {
      topMostIndexPath = indexPath;
      break;
    }
  }

  return topMostIndexPath.row;
}

- (void)navigateAway {
  [self.editingFolderCell stopEdit];
}

// Returns YES if the given node is a url or folder node.
- (BOOL)isUrlOrFolder:(const BookmarkNode*)node {
  return node->type() == BookmarkNode::URL ||
         node->type() == BookmarkNode::FOLDER;
}

// Returns YES if the given node can be edited by user.
- (BOOL)isNodeEditableByUser:(const BookmarkNode*)node {
  CHECK(node, base::NotFatalUntil::M155);
  // Note that IsNodeManaged() below returns false for Bookmarks Bar, Mobile
  // Bookmarks, and Other Bookmarks since the user can add, delete, and edit
  // items within those folders. IsNodeManaged() returns true for the
  // managed_node and all nodes that are descendants of managed_node.
  bookmarks::ManagedBookmarkService* managedBookmarkService =
      ManagedBookmarkServiceFactory::GetForProfile(self.profile);
  return managedBookmarkService ? !managedBookmarkService->IsNodeManaged(node)
                                : YES;
}

// Returns YES if user is allowed to edit any bookmarks.
- (BOOL)isEditBookmarksEnabled {
  ProfileIOS* profile = self.profile;
  if (!profile) {
    // The view is being closed.
    return NO;
  }
  return profile->GetPrefs()->GetBoolean(
      bookmarks::prefs::kEditBookmarksEnabled);
}

// Returns the bookmark node associated with `indexPath`. Returns nil if the
// node has been deleted.
- (const BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath {
  if (!_bookmarkModel) {
    return nullptr;
  }
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];

  if (item.type == BookmarksHomeItemTypeBookmark) {
    BookmarksHomeNodeItem* nodeItem =
        base::apple::ObjCCastStrict<BookmarksHomeNodeItem>(item);
    return [nodeItem bookmarkNode:_bookmarkModel.get()];
  }

  NOTREACHED(base::NotFatalUntil::M152) << "Unexpected item type " << item.type;
  return nullptr;
}

- (BOOL)hasItemAtIndexPath:(NSIndexPath*)indexPath {
  return [self.tableViewModel hasItemAtIndexPath:indexPath];
}

// Whether the view is currently displaying bookmarks or folders.
- (BOOL)hasBookmarksOrFolders {
  if (!self.mediator.displayedNode) {
    return NO;
  }
  if (self.mediator.currentlyShowingSearchResults) {
    return [self
        hasItemsInSectionIdentifier:BookmarksHomeSectionIdentifierBookmarks];
  } else {
    return !self.mediator.displayedNode->children().empty();
  }
}

- (BOOL)hasItemsInSectionIdentifier:(NSInteger)sectionIdentifier {
  BOOL hasSection =
      [self.tableViewModel hasSectionForSectionIdentifier:sectionIdentifier];
  if (!hasSection) {
    return NO;
  }
  NSInteger section =
      [self.tableViewModel sectionForSectionIdentifier:sectionIdentifier];
  return [self.tableViewModel numberOfItemsInSection:section] > 0;
}

- (std::vector<const BookmarkNode*>)selectedNodesForEditMode {
  std::vector<const BookmarkNode*> nodes;
  if (self.mediator.currentlyShowingSearchResults) {
    // Create a vector of edit nodes in the same order as the selected nodes.
    std::ranges::copy(self.mediator.selectedNodesForEditMode,
                      std::back_inserter(nodes));
  } else {
    // Create a vector of edit nodes in the same order as the nodes in folder.
    for (const auto& child : self.mediator.displayedNode->children()) {
      if (self.mediator.selectedNodesForEditMode.contains(child.get())) {
        nodes.push_back(child.get());
      }
    }
  }
  return nodes;
}

// Dismiss the search controller when there's a touch event on the scrim.
- (void)dismissSearchController:(UIControl*)sender {
  if (self.searchController.active) {
    self.searchController.active = NO;
  }
}

// Show scrim overlay and hide toolbar.
- (void)showScrim {
  self.navigationController.toolbarHidden = YES;
  UIView* scrimView = self.scrimView;
  UITableView* tableView = self.tableView;
  UIView* superview = tableView.superview;
  scrimView.alpha = 0.0f;
  [tableView addSubview:scrimView];
  // We attach our constraints to the superview because the tableView is
  // a scrollView and it seems that we get an empty frame when attaching to it.
  if (@available(iOS 26, *)) {
    // On iOS 26+, the search bar won't be obscured by the scrim view even when
    // the scrim view's top constraint is aligned with the superview's top,
    // likely due to changes in UIKit's layout system or view hierarchy
    // handling.
    AddSameConstraints(scrimView, superview);
  } else {
    [NSLayoutConstraint activateConstraints:@[
      [scrimView.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor],
      [scrimView.trailingAnchor
          constraintEqualToAnchor:superview.trailingAnchor],
      [scrimView.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor],
      [scrimView.topAnchor
          constraintEqualToAnchor:self.navigationController.navigationBar
                                      .bottomAnchor],
    ]];
  }
  tableView.accessibilityElementsHidden = YES;
  tableView.scrollEnabled = NO;
  [UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
                   animations:^{
                     scrimView.alpha = 1.0f;
                     [superview layoutIfNeeded];
                   }];
}

// Hide scrim and restore toolbar.
- (void)hideScrim {
  __weak BookmarksHomeViewController* weakSelf = self;
  [UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
      animations:^{
        weakSelf.scrimView.alpha = 0.0f;
      }
      completion:^(BOOL finished) {
        [weakSelf hideScrimCompletion];
      }];
  [self setupContextBar];
}

- (void)hideScrimCompletion {
  [self.scrimView removeFromSuperview];
  self.tableView.accessibilityElementsHidden = NO;
  self.tableView.scrollEnabled = YES;
}

- (BOOL)scrimIsVisible {
  return self.scrimView.superview ? YES : NO;
}

// Returns a bookmark node (URL or folder) with `nodeID`, or nil if no node
// exists with such ID.
- (const BookmarkNode*)findNodeByID:(int64_t)nodeID {
  return bookmarks::GetBookmarkNodeByID(_bookmarkModel.get(), nodeID);
}

// Triggers the URL sharing flow for `bookmarkNodeID` node, if it still exists.
- (void)shareURLBookmarkNodeWithID:(int64_t)nodeID
                         indexPath:(NSIndexPath*)indexPath {
  const BookmarkNode* bookmarkNode = [self findNodeByID:nodeID];
  if (!bookmarkNode) {
    // While the contextual menu was opened, the node might has been removed.
    // If the node doesn't exist anymore, there nothing to do.
    return;
  }
  DCHECK(bookmarkNode->is_url());
  GURL bookmarkURL = bookmarkNode->url();
  NSString* title = bookmark_utils_ios::TitleForBookmarkNode(bookmarkNode);
  SharingParams* params =
      [[SharingParams alloc] initWithURL:bookmarkURL
                                   title:title
                                scenario:SharingScenario::BookmarkEntry];
  UIView* cellView = [self.tableView cellForRowAtIndexPath:indexPath];
  [self.sharingCoordinator stop];
  self.sharingCoordinator =
      [[SharingCoordinator alloc] initWithBaseViewController:self
                                                     browser:_browser.get()
                                                      params:params
                                                  sourceItem:cellView];
  [self.sharingCoordinator start];
}

// Returns whether the incognito mode is forced.
- (BOOL)isIncognitoForced {
  return IsIncognitoModeForced(self.profile->GetPrefs());
}

// Returns whether the incognito mode is available.
- (BOOL)isIncognitoAvailable {
  return !IsIncognitoModeDisabled(self.profile->GetPrefs());
}

#pragma mark - Loading and Empty States

// Shows loading spinner background view.
- (void)showLoadingSpinnerBackground {
  if (!self.spinnerView) {
    self.spinnerView =
        [[HomeWaitingView alloc] initWithFrame:self.tableView.bounds
                               backgroundColor:UIColor.clearColor];
    [self.spinnerView startWaiting];
  }
  self.tableView.backgroundView = self.spinnerView;
}

// Hide the loading spinner if it is showing.
- (void)hideLoadingSpinnerBackground {
  if (self.spinnerView) {
    __weak BookmarksHomeViewController* weakSelf = self;
    [self.spinnerView stopWaitingWithCompletion:^{
      [UIView animateWithDuration:0.2
          animations:^{
            weakSelf.spinnerView.alpha = 0.0;
          }
          completion:^(BOOL finished) {
            [weakSelf
                hideLoadingSpinnerBackgroundCompletionWithFinished:finished];
          }];
    }];
  }
}

- (void)hideLoadingSpinnerBackgroundCompletionWithFinished:(BOOL)finished {
  // By the time completion block is called, the backgroundView could
  // be another view, like the empty view background. Only clear the
  // background if it is still the spinner.
  if (self.tableView.backgroundView == self.spinnerView) {
    self.tableView.backgroundView = nil;
  }
  self.spinnerView = nil;
}

// Shows empty bookmarks background view.
- (void)showEmptyBackground {
  if (!self.emptyViewBackground) {
    self.emptyViewBackground = [[TableViewIllustratedEmptyView alloc]
        initWithFrame:self.tableView.bounds
                image:[UIImage imageNamed:IsChromeNextIaEnabled()
                                              ? @"bookmark_empty"
                                              : @"bookmark_empty_legacy"]
                title:GetNSString(IDS_IOS_BOOKMARK_EMPTY_TITLE)
             subtitle:GetNSString(IDS_IOS_BOOKMARK_EMPTY_MESSAGE)];
  }
  // If the Signin promo is visible on the root view, we have to shift the
  // empty TableView background to make it fully visible on all devices.
  if ([self isDisplayingBookmarkRoot]) {
    // Reload the data to ensure consistency between the model and the table
    // (an example scenario can be found at crbug.com/1116408). Reloading the
    // data should only be done for the root bookmark folder since it can be
    // very expensive in other folders.
    [self.tableView reloadData];

    self.navigationItem.largeTitleDisplayMode =
        UINavigationItemLargeTitleDisplayModeNever;
    if (self.mediator.promoVisible && self.tableView.visibleCells.count) {
      CGFloat signinPromoHeight =
          self.tableView.visibleCells.firstObject.bounds.size.height;
      self.emptyViewBackground.scrollViewContentInsets =
          UIEdgeInsetsMake(signinPromoHeight, 0.0, 0.0, 0.0);
    } else {
      self.emptyViewBackground.scrollViewContentInsets =
          self.view.safeAreaInsets;
    }
  }

  self.tableView.backgroundView = self.emptyViewBackground;
  self.navigationItem.searchController = nil;
}

- (void)hideEmptyBackground {
  if (self.tableView.backgroundView == self.emptyViewBackground) {
    self.tableView.backgroundView = nil;
  }
  self.navigationItem.searchController = self.searchController;
  if ([self isDisplayingBookmarkRoot]) {
    self.navigationItem.largeTitleDisplayMode =
        UINavigationItemLargeTitleDisplayModeAutomatic;
  }
}

#pragma mark - ContextBarDelegate implementation

// Called when the leading button is clicked.
- (void)leadingButtonClicked {
  // Ignore the button tap if any of our controllers is presenting.
  if ([self isAnyControllerPresenting]) {
    return;
  }
  const std::set<const BookmarkNode*> nodes =
      self.mediator.selectedNodesForEditMode;
  switch (self.contextBarState) {
    case BookmarksContextBarDefault:
      // New Folder clicked.
      [self addNewFolder];
      break;
    case BookmarksContextBarBeginSelection:
      // This must never happen, as the leading button is disabled at this
      // point.
      NOTREACHED();
    case BookmarksContextBarSingleURLSelection:
    case BookmarksContextBarMultipleURLSelection:
    case BookmarksContextBarSingleFolderSelection:
    case BookmarksContextBarMultipleFolderSelection:
    case BookmarksContextBarMixedSelection:
      // Delete clicked.
      [self deleteBookmarkNodes:nodes
                     userAction:"MobileBookmarkManagerRemoveSelected"];
      break;
    case BookmarksContextBarNone:
    default:
      NOTREACHED();
  }
}

// Called when the center button is clicked.
- (void)centerButtonClicked {
  // Ignore the button tap if any of our controller is presenting.
  if ([self isAnyControllerPresenting]) {
    return;
  }
  const std::set<const BookmarkNode*> nodes =
      self.mediator.selectedNodesForEditMode;
  // Center button is shown and is clickable only when at least
  // one node is selected.
  DCHECK(nodes.size() > 0);

  self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
      initWithBaseViewController:self
                         browser:_browser.get()
                           title:nil
                         message:nil
                   barButtonItem:self.moreButton];

  switch (self.contextBarState) {
    case BookmarksContextBarSingleURLSelection:
      [self configureCoordinator:self.actionSheetCoordinator
            forSingleBookmarkURL:*(nodes.begin())];
      break;
    case BookmarksContextBarMultipleURLSelection:
      [self configureCoordinator:self.actionSheetCoordinator
          forMultipleBookmarkURLs:nodes];
      break;
    case BookmarksContextBarSingleFolderSelection:
      [self configureCoordinator:self.actionSheetCoordinator
          forSingleBookmarkFolder:*(nodes.begin())];
      break;
    case BookmarksContextBarMultipleFolderSelection:
    case BookmarksContextBarMixedSelection:
      [self configureCoordinator:self.actionSheetCoordinator
          forMixedAndMultiFolderSelection:nodes];
      break;
    case BookmarksContextBarDefault:
    case BookmarksContextBarBeginSelection:
    case BookmarksContextBarNone:
      // Center button is disabled in these states.
      NOTREACHED();
  }

  [self addCancelActionToCoordinator:self.actionSheetCoordinator];
  [self.actionSheetCoordinator start];
}

// Called when the trailing button, "Select" or "Done" is clicked.
- (void)trailingButtonClicked {
  // Ignore the button tap if any of our controller is presenting.
  if ([self isAnyControllerPresenting]) {
    return;
  }
  // Toggle edit mode.
  [self setTableViewEditing:!self.mediator.currentlyInEditMode];
}

// Displays the UITableView edit mode and selects the row containing the
// `_externalBookmark`.
- (void)editExternalBookmarkIfSet {
  if (!_bookmarkModel || !_externalBookmark) {
    return;
  }
  bookmarks::BookmarkModel* bookmarkModel = _bookmarkModel.get();

  [self setTableViewEditing:YES];
  NSArray<NSIndexPath*>* paths = [self.tableViewModel
      indexPathsForItemType:BookmarksHomeItemTypeBookmark
          sectionIdentifier:BookmarksHomeSectionIdentifierBookmarks];
  for (id path in paths) {
    BookmarksHomeNodeItem* node =
        base::apple::ObjCCastStrict<BookmarksHomeNodeItem>(
            [self.tableViewModel itemAtIndexPath:path]);
    if ([node bookmarkNode:bookmarkModel] == _externalBookmark) {
      [self.tableView selectRowAtIndexPath:path
                                  animated:NO
                            scrollPosition:UITableViewScrollPositionMiddle];
      [self.tableView.delegate tableView:self.tableView
                 didSelectRowAtIndexPath:path];
      break;
    }
  }
}

#pragma mark - ContextBarStates

// Sets the context bar buttons in their default state (non-edit mode).
- (void)setBookmarksContextBarButtonsDefaultState {
  [self setToolbarItems:@[
    [self createNewFolderButton], [self createSpacerButton],
    [self createEditButton]
  ]
               animated:YES];
}

// Sets the context bar buttons in edit mode.
- (void)setBookmarksContextBarSelectionStartState {
  BOOL isCurrentlyShowingSearchResults =
      self.mediator.currentlyShowingSearchResults;

  [self setToolbarItems:isCurrentlyShowingSearchResults ? @[
        self.deleteButton, [self createSpacerButton], self.moreButton,
        [self createSpacerButton], [self createDoneButton]
      ] : @[
        self.deleteButton, [self createSpacerButton], self.moreButton
      ] animated:YES];
}

// Sets the default navigation bar buttons.
- (void)setBookmarksNavigationBarButtonsDefaultState {
  // Restores the default back button.
  self.navigationItem.leftBarButtonItem = nil;
  self.navigationItem.leftBarButtonItem.action = @selector(backAction);
  self.navigationItem.rightBarButtonItem = [self createNavigationBarDoneButton];
}

// Sets the navigation bar buttons in edit mode.
- (void)setBookmarksNavigationBarSelectionState {
  self.navigationItem.leftBarButtonItem = [self createMultiSelectButton];
  self.navigationItem.rightBarButtonItem = [self createNavigationBarDoneButton];
}

#pragma mark - Context Menu

- (void)configureCoordinator:(AlertCoordinator*)coordinator
     forMultipleBookmarkURLs:(const std::set<const BookmarkNode*>)nodes {
  __weak BookmarksHomeViewController* weakSelf = self;
  coordinator.alertController.view.accessibilityIdentifier =
      kBookmarksHomeContextMenuIdentifier;

  NSString* titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_OPEN);
  [coordinator addItemWithTitle:titleString
                         action:^{
                           [weakSelf actionSheetOpenAction];
                         }
                          style:UIAlertActionStyleDefault
                        enabled:![self isIncognitoForced]];

  titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_OPEN_INCOGNITO);
  [coordinator addItemWithTitle:titleString
                         action:^{
                           [weakSelf actionSheetOpenIncognitoAction];
                         }
                          style:UIAlertActionStyleDefault
                        enabled:[self isIncognitoAvailable]];

  const BookmarkNodeIDSet nodeIDs = GetBookmarkNodeIDSet(nodes);

  titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE);
  [coordinator addItemWithTitle:titleString
                         action:^{
                           [weakSelf actionSheetMoveActionWithNodeIDs:nodeIDs];
                         }
                          style:UIAlertActionStyleDefault];
}

- (void)configureCoordinator:(AlertCoordinator*)coordinator
        forSingleBookmarkURL:(const BookmarkNode*)node {
  __weak BookmarksHomeViewController* weakSelf = self;
  const GURL nodeURL = node->url();
  const int64_t nodeID = node->id();
  const std::string urlString = nodeURL.possibly_invalid_spec();
  coordinator.alertController.view.accessibilityIdentifier =
      kBookmarksHomeContextMenuIdentifier;
  NSString* titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_EDIT);
  // Disable the edit menu option if the node is not editable by user, or if
  // editing bookmarks is not allowed.
  BOOL editEnabled =
      [self isEditBookmarksEnabled] && [self isNodeEditableByUser:node];

  [coordinator addItemWithTitle:titleString
                         action:^{
                           [weakSelf actionSheetEditActionWithNodeID:nodeID];
                         }
                          style:UIAlertActionStyleDefault
                        enabled:editEnabled];

  titleString = GetNSString(IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB);
  [coordinator
      addItemWithTitle:titleString
                action:^{
                  [weakSelf actionSheetOpenLinkNewTabActionWithNodeURL:nodeURL];
                }
                 style:UIAlertActionStyleDefault
               enabled:![self isIncognitoForced]];

  if (base::ios::IsMultipleScenesSupported()) {
    titleString = GetNSString(IDS_IOS_CONTENT_CONTEXT_OPENINNEWWINDOW);
    auto action = ^{
      [weakSelf actionSheetOpenNewWindowActionWithURL:nodeURL];
    };
    [coordinator addItemWithTitle:titleString
                           action:action
                            style:UIAlertActionStyleDefault];
  }

  titleString = GetNSString(IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB);
  [coordinator
      addItemWithTitle:titleString
                action:^{
                  [weakSelf actionSheetOpenLinkNewIncognitoTabWithURL:nodeURL];
                }
                 style:UIAlertActionStyleDefault
               enabled:[self isIncognitoAvailable]];

  titleString = GetNSString(IDS_IOS_CONTENT_CONTEXT_COPY);
  [coordinator
      addItemWithTitle:titleString
                action:^{
                  [weakSelf actionSheetCopyActionWithURLString:urlString];
                }
                 style:UIAlertActionStyleDefault];
}

- (void)configureCoordinator:(AlertCoordinator*)coordinator
     forSingleBookmarkFolder:(const BookmarkNode*)node {
  __weak BookmarksHomeViewController* weakSelf = self;
  const int64_t nodeID = node->id();
  coordinator.alertController.view.accessibilityIdentifier =
      kBookmarksHomeContextMenuIdentifier;

  NSString* titleString =
      GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_EDIT_FOLDER);
  // Disable the edit and move menu options if the folder is not editable by
  // user, or if editing bookmarks is not allowed.
  BOOL editEnabled =
      [self isEditBookmarksEnabled] && [self isNodeEditableByUser:node];

  [coordinator
      addItemWithTitle:titleString
                action:^{
                  [weakSelf actionSheetEditFolderActionWithNodeID:nodeID];
                }
                 style:UIAlertActionStyleDefault
               enabled:editEnabled];

  titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE);
  [coordinator addItemWithTitle:titleString
                         action:^{
                           [weakSelf actionSheetMoveActionWithNodeID:nodeID];
                         }
                          style:UIAlertActionStyleDefault
                        enabled:editEnabled];
}

- (void)configureCoordinator:(AlertCoordinator*)coordinator
    forMixedAndMultiFolderSelection:(const std::set<const BookmarkNode*>)nodes {
  __weak BookmarksHomeViewController* weakSelf = self;
  coordinator.alertController.view.accessibilityIdentifier =
      kBookmarksHomeContextMenuIdentifier;

  const BookmarkNodeIDSet nodeIDs = GetBookmarkNodeIDSet(nodes);
  NSString* titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE);
  [coordinator addItemWithTitle:titleString
                         action:^{
                           [weakSelf actionSheetMoveActionWithNodeIDs:nodeIDs];
                         }
                          style:UIAlertActionStyleDefault];
}

- (void)addCancelActionToCoordinator:(AlertCoordinator*)coordinator {
  __weak BookmarksHomeViewController* weakSelf = self;
  [self.actionSheetCoordinator
      addItemWithTitle:GetNSString(IDS_APP_CANCEL)
                action:^{
                  [weakSelf dismissActionSheetCoordinator];
                }
                 style:UIAlertActionStyleCancel];
}

#pragma mark - Context Menu helpers

- (void)actionSheetOpenAction {
  [self dismissActionSheetCoordinator];
  std::vector<const BookmarkNode*> selectedNodesForEditMode =
      [self selectedNodesForEditMode];
  [self openAllURLs:GetUrlsToOpen(selectedNodesForEditMode)
        inIncognito:NO
             newTab:NO];
}

- (void)actionSheetOpenIncognitoAction {
  [self dismissActionSheetCoordinator];
  std::vector<const BookmarkNode*> selectedNodesForEditMode =
      [self selectedNodesForEditMode];
  [self openAllURLs:GetUrlsToOpen(selectedNodesForEditMode)
        inIncognito:YES
             newTab:NO];
}

- (void)actionSheetMoveActionWithNodeIDs:(BookmarkNodeIDSet)nodeIDs {
  [self dismissActionSheetCoordinator];
  [self moveBookmarkNodeWithIDs:nodeIDs
                     userAction:"MobileBookmarkManagerMove"
                                "ToFolderBulk"];
}

- (void)actionSheetEditActionWithNodeID:(const int64_t)nodeID {
  [self dismissActionSheetCoordinator];
  [self editBookmarkNodeWithID:nodeID];
}

- (void)actionSheetOpenLinkNewTabActionWithNodeURL:(const GURL)nodeURL {
  [self dismissActionSheetCoordinator];
  [self openAllURLs:{nodeURL} inIncognito:NO newTab:YES];
}

- (void)actionSheetOpenNewWindowActionWithURL:(const GURL)nodeURL {
  [self dismissActionSheetCoordinator];
  [self.sceneHandler
      openNewWindowWithActivity:ActivityToLoadURL(WindowActivityBookmarksOrigin,
                                                  nodeURL)];
}

- (void)actionSheetOpenLinkNewIncognitoTabWithURL:(const GURL)nodeURL {
  [self dismissActionSheetCoordinator];
  [self openAllURLs:{nodeURL} inIncognito:YES newTab:YES];
}

- (void)actionSheetCopyActionWithURLString:(const std::string)urlString {
  [self dismissActionSheetCoordinator];
  [self setTableViewEditing:NO];
  StoreTextInPasteboard(base::SysUTF8ToNSString(urlString));
}

- (void)actionSheetEditFolderActionWithNodeID:(const int64_t)nodeID {
  [self dismissActionSheetCoordinator];
  [self editFolderNodeWithID:nodeID];
}

- (void)actionSheetMoveActionWithNodeID:(const int64_t)nodeID {
  [self dismissActionSheetCoordinator];
  [self moveBookmarkNodeWithIDs:{nodeID}
                     userAction:"MobileBookmarkManagerMove"
                                "ToFolder"];
}

#pragma mark - UIGestureRecognizerDelegate and gesture handling

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer {
  if (gestureRecognizer ==
      self.navigationController.interactivePopGestureRecognizer) {
    return self.navigationController.viewControllers.count > 1;
  }
  return YES;
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
       shouldReceiveTouch:(UITouch*)touch {
  // Ignore long press in edit mode or search mode.
  if (self.mediator.currentlyInEditMode || [self scrimIsVisible]) {
    return NO;
  }
  return YES;
}

- (void)handleLongPress:(UILongPressGestureRecognizer*)gestureRecognizer {
  if (_UIDisabled || self.mediator.currentlyInEditMode ||
      gestureRecognizer.state != UIGestureRecognizerStateBegan) {
    return;
  }
  CGPoint touchPoint = [gestureRecognizer locationInView:self.tableView];
  NSIndexPath* indexPath = [self.tableView indexPathForRowAtPoint:touchPoint];

  if (![self canShowContextMenuFor:indexPath]) {
    return;
  }

  const BookmarkNode* node = [self nodeAtIndexPath:indexPath];

  self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
      initWithBaseViewController:self
                         browser:_browser.get()
                           title:nil
                         message:nil
                            rect:CGRectMake(touchPoint.x, touchPoint.y, 1, 1)
                            view:self.tableView];

  if (node->is_url()) {
    [self configureCoordinator:self.actionSheetCoordinator
          forSingleBookmarkURL:node];
  } else if (node->is_folder()) {
    [self configureCoordinator:self.actionSheetCoordinator
        forSingleBookmarkFolder:node];
  } else {
    NOTREACHED();
  }

  [self addCancelActionToCoordinator:self.actionSheetCoordinator];
  [self.actionSheetCoordinator start];
}

- (BOOL)canShowContextMenuFor:(NSIndexPath*)indexPath {
  if (indexPath == nil ||
      [self.tableViewModel
          sectionIdentifierForSectionIndex:indexPath.section] !=
          BookmarksHomeSectionIdentifierBookmarks) {
    return NO;
  }

  const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
  // Don't show context menus for permanent nodes, which include Bookmarks Bar,
  // Mobile Bookmarks, Other Bookmarks, and Managed Bookmarks. Permanent nodes
  // do not include descendants of Managed Bookmarks. Also, context menus are
  // only supported on URLs or folders.
  return node && !node->is_permanent_node() &&
         (node->is_url() || node->is_folder());
}

#pragma mark UISearchResultsUpdating

- (void)updateSearchResultsForSearchController:
    (UISearchController*)searchController {
  CHECK_EQ(self.searchController, searchController, base::NotFatalUntil::M152);
  NSString* text = searchController.searchBar.text;
  self.searchTerm = text;

  if (text.length == 0) {
    if (self.mediator.currentlyShowingSearchResults) {
      self.mediator.currentlyShowingSearchResults = NO;
      // Restore current list.
      [self.mediator computeBookmarkTableViewData];
      [self.mediator computePromoTableViewData];
      [self.tableView reloadData];
      [self showScrim];
    }
  } else {
    if (!self.mediator.currentlyShowingSearchResults) {
      self.mediator.currentlyShowingSearchResults = YES;
      [self.mediator computePromoTableViewData];
      [self hideScrim];
    }
    // Replace current list with search result, but doesn't change
    // the 'regular' model for this page, which we can restore when search
    // is terminated.
    NSString* noResults = GetNSString(IDS_HISTORY_NO_SEARCH_RESULTS);
    [self.mediator computeBookmarkTableViewDataMatching:text
                             orShowMessageWhenNoResults:noResults];
    [self.tableView reloadData];
    [self setupContextBar];
  }
}

#pragma mark UISearchControllerDelegate

- (void)willPresentSearchController:(UISearchController*)searchController {
  [self showScrim];
}

- (void)willDismissSearchController:(UISearchController*)searchController {
  // Avoid scrim being put back on in updateSearchResultsForSearchController.
  self.mediator.currentlyShowingSearchResults = NO;
  // Restore current list.
  [self.mediator computeBookmarkTableViewData];
  [self.tableView reloadData];
}

- (void)didDismissSearchController:(UISearchController*)searchController {
  [self hideScrim];
}

#pragma mark - UITableViewDataSource

- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];

  if (item.type == BookmarksHomeItemTypeBookmark) {
    // Load the favicon from cache. If not found, try fetching it from a
    // Google Server.
    [self loadFaviconAtIndexPath:indexPath fallbackToGoogleServer:YES];
  }

  UITableViewCell* cell = [super tableView:tableView
                     cellForRowAtIndexPath:indexPath];
  UIView* selectedBackgroundView = [[UIView alloc] init];
  selectedBackgroundView.backgroundColor =
      [UIColor colorNamed:kTertiaryBackgroundColor];
  cell.selectedBackgroundView = selectedBackgroundView;

  cell.userInteractionEnabled =
      (item.type != BookmarksHomeItemTypeMessage &&
       item.type != BookmarksHomeItemTypeBatchUploadRecommendation);

  if (item.type == BookmarksHomeItemTypeBookmark) {
    BookmarksHomeNodeItem* nodeItem =
        base::apple::ObjCCastStrict<BookmarksHomeNodeItem>(item);
    const bookmarks::BookmarkNode* node =
        _bookmarkModel ? [nodeItem bookmarkNode:_bookmarkModel.get()] : nullptr;
    if (node && node->is_folder() && node == self.mediator.editingFolderNode) {
      TableViewBookmarksFolderCell* tableCell =
          base::apple::ObjCCastStrict<TableViewBookmarksFolderCell>(cell);
      // Delay starting edit, so that the cell is fully created. This is
      // needed when scrolling away and then back into the editingCell,
      // without the delay the cell will resign first responder before its
      // created.
      __weak BookmarksHomeViewController* weakSelf = self;
      dispatch_async(dispatch_get_main_queue(), ^{
        [weakSelf editCellWithTableCell:tableCell];
      });
    }
  }

  return cell;
}

- (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath {
  if (!_bookmarkModel) {
    return NO;
  }

  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
  if (item.type != BookmarksHomeItemTypeBookmark) {
    // Can only edit bookmarks.
    return NO;
  }

  // If the cell at `indexPath` is being edited (which happens when creating a
  // new Folder) return NO.
  if ([tableView indexPathForCell:self.editingFolderCell] == indexPath) {
    return NO;
  }

  // Enable the swipe-to-delete gesture and reordering control for editable
  // nodes of type URL or Folder, but not the permanent ones. Only enable
  // swipe-to-delete if editing bookmarks is allowed.
  BookmarksHomeNodeItem* nodeItem =
      base::apple::ObjCCastStrict<BookmarksHomeNodeItem>(item);
  const BookmarkNode* node = [nodeItem bookmarkNode:_bookmarkModel.get()];
  return node && [self isEditBookmarksEnabled] && [self isUrlOrFolder:node] &&
         [self isNodeEditableByUser:node];
}

- (void)tableView:(UITableView*)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
     forRowAtIndexPath:(NSIndexPath*)indexPath {
  if (!_bookmarkModel) {
    return;
  }
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
  if (item.type != BookmarksHomeItemTypeBookmark) {
    // Can only commit edits for bookmarks.
    return;
  }

  if (editingStyle == UITableViewCellEditingStyleDelete) {
    BookmarksHomeNodeItem* nodeItem =
        base::apple::ObjCCastStrict<BookmarksHomeNodeItem>(item);
    const BookmarkNode* node = [nodeItem bookmarkNode:_bookmarkModel.get()];
    if (!node) {
      return;
    }
    std::set<const BookmarkNode*> nodes;
    nodes.insert(node);
    [self deleteBookmarkNodes:nodes
                   userAction:"MobileBookmarkManagerEntryDeleted"];
  }
}

- (BOOL)tableView:(UITableView*)tableView
    canMoveRowAtIndexPath:(NSIndexPath*)indexPath {
  // No reorering with filtered results or when displaying the top-most
  // Bookmarks node.
  if (self.mediator.currentlyShowingSearchResults ||
      [self isDisplayingBookmarkRoot] || !self.tableView.editing) {
    return NO;
  }
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
  if (item.type != BookmarksHomeItemTypeBookmark) {
    // Can only move bookmarks.
    return NO;
  }

  return YES;
}

- (void)tableView:(UITableView*)tableView
    moveRowAtIndexPath:(NSIndexPath*)sourceIndexPath
           toIndexPath:(NSIndexPath*)destinationIndexPath {
  if (sourceIndexPath.row == destinationIndexPath.row ||
      self.mediator.currentlyShowingSearchResults ||
      !self.displayedFolderNode) {
    return;
  }
  const BookmarkNode* node = [self nodeAtIndexPath:sourceIndexPath];
  // Calculations: Assume we have 3 nodes A B C. Node positions are A(0), B(1),
  // C(2) respectively. When we move A to after C, we are moving node at index 0
  // to 3 (position after C is 3, in terms of the existing contents). Hence add
  // 1 when moving forward. When moving backward, if C(2) is moved to Before B,
  // we move node at index 2 to index 1 (position before B is 1, in terms of the
  // existing contents), hence no change in index is necessary. It is required
  // to make these adjustments because this is how bookmark_model handles move
  // operations.
  size_t newPosition = sourceIndexPath.row < destinationIndexPath.row
                           ? destinationIndexPath.row + 1
                           : destinationIndexPath.row;

  SnackbarMessage* snackbarMessage =
      bookmark_utils_ios::UpdateBookmarkPositionWithUndoSnackbar(
          node, self.displayedFolderNode, newPosition, _bookmarkModel.get(),
          self.profile);
  if (snackbarMessage) {
    [self.snackbarCommandsHandler showSnackbarMessage:snackbarMessage];
  }
}

#pragma mark - UITableViewDataSource Helper

- (void)editCellWithTableCell:(TableViewBookmarksFolderCell*)tableCell {
  self.editingFolderCell = tableCell;
  [tableCell startEdit];
  tableCell.textDelegate = self;
}

#pragma mark - UITableViewDelegate

- (UISwipeActionsConfiguration*)tableView:(UITableView*)tableView
    trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath*)indexPath {
  const BookmarkNode* bookmarkNode = [self nodeAtIndexPath:indexPath];

  if (!bookmarkNode) {
    return nil;
  }

  NSMutableArray<UIContextualAction*>* actions = [NSMutableArray array];

  BOOL canEditNode =
      [self isNodeEditableByUser:bookmarkNode] && [self isEditBookmarksEnabled];

  if (canEditNode) {
    UIContextualAction* deleteAction =
        [self createDeleteActionForIndexPath:indexPath];
    [actions addObject:deleteAction];
  }

  if (send_tab_to_self::AreIOSTabRemindersEnabled() && bookmarkNode->is_url()) {
    UIContextualAction* remindAction =
        [self createRemindActionForIndexPath:indexPath];
    [actions addObject:remindAction];
  }

  if (!actions.count) {
    return nil;
  }

  UISwipeActionsConfiguration* configuration =
      [UISwipeActionsConfiguration configurationWithActions:actions];

  // A full swipe automatically performs the first action (Delete).
  configuration.performsFirstActionWithFullSwipe = canEditNode;

  return configuration;
}

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  if (_UIDisabled) {
    // Deselect row.
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    return;
  }
  BookmarksHomeSectionIdentifier sectionIdentifier =
      (BookmarksHomeSectionIdentifier)([self.tableViewModel
          sectionIdentifierForSectionIndex:indexPath.section]);
  if (IsABookmarkNodeSectionForIdentifier(sectionIdentifier)) {
    const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
    if (!node) {
      // If the user tapped on an entry and the list changed before the current
      // method is called, that cause a wrong entry to be opened. In particular
      // if the last entry was tapped and an entry is removed, the node don’t
      // exists. Let’s just do nothing in this last case.
      [tableView deselectRowAtIndexPath:indexPath animated:YES];
      return;
    }
    // If table is in edit mode, record all the nodes added to edit set.
    if (self.mediator.currentlyInEditMode) {
      if ([self isNodeEditableByUser:node]) {
        // Only add nodes that are editable to the edit set.
        self.mediator.selectedNodesForEditMode.insert(node);
        [self handleSelectEditNodes:self.mediator.selectedNodesForEditMode];
        return;
      }
      // If the selected row is not editable, do not add it to the edit set.
      // Simply deselect the row.
      [tableView deselectRowAtIndexPath:indexPath animated:YES];
      return;
    }
    [self.editingFolderCell stopEdit];
    if (node->is_folder()) {
      base::RecordAction(
          base::UserMetricsAction("MobileBookmarkManagerOpenFolder"));
      [self handleSelectFolderForNavigation:node];
    } else {
      if (self.mediator.currentlyShowingSearchResults) {
        // Set the searchController active property to NO or the SearchBar will
        // cause the navigation controller to linger for a second  when
        // dismissing.
        self.searchController.active = NO;
      }
      // Open URL. Pass this to the delegate.
      [self handleSelectUrlForNavigation:node->url()];
    }
  } else if (sectionIdentifier == BookmarksBatchUploadSectionIdentifier) {
    // Open batch upload alert dialog if batch upload button clicked.
    TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
    if (static_cast<BookmarksHomeItemType>(item.type) ==
        BookmarksHomeItemTypeBatchUploadButton) {
      base::RecordAction(base::UserMetricsAction(
          "MobileBookmarksManagerBulkSaveBookmarksToAccountButtonClicked"));
      CGRect targetRect = [tableView rectForRowAtIndexPath:indexPath];
      [self showBatchUploadDialog:targetRect];
    }
  }
  // Deselect row.
  [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

- (void)tableView:(UITableView*)tableView
    didDeselectRowAtIndexPath:(NSIndexPath*)indexPath {
  BookmarksHomeSectionIdentifier sectionIdentifier =
      (BookmarksHomeSectionIdentifier)[self.tableViewModel
          sectionIdentifierForSectionIndex:indexPath.section];
  if (sectionIdentifier == BookmarksHomeSectionIdentifierBookmarks &&
      self.mediator.currentlyInEditMode) {
    const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
    CHECK(node, base::NotFatalUntil::M152);
    if (!node) {
      return;
    }
    self.mediator.selectedNodesForEditMode.erase(node);
    [self handleSelectEditNodes:self.mediator.selectedNodesForEditMode];
  }
}

- (UIContextMenuConfiguration*)tableView:(UITableView*)tableView
    contextMenuConfigurationForRowAtIndexPath:(NSIndexPath*)indexPath
                                        point:(CGPoint)point {
  // TODO(crbug.com/428177163): Remove this workaround when the underlying iOS
  // issue handling context menu presentation during an active drag/drop session
  // is resolved.
  if (tableView.hasActiveDrag || tableView.hasActiveDrop) {
    return nil;
  }
  if (self.mediator.currentlyInEditMode) {
    // Don't show the context menu when currently in editing mode.
    return nil;
  }
  if (![self canShowContextMenuFor:indexPath]) {
    return nil;
  }

  const BookmarkNode* node = [self nodeAtIndexPath:indexPath];

  // Disable the edit and move menu options if the node is not editable by user,
  // or if editing bookmarks is not allowed.
  BOOL canEditNode =
      [self isEditBookmarksEnabled] && [self isNodeEditableByUser:node];
  UIContextMenuActionProvider actionProvider;

  __weak BookmarksHomeViewController* weakSelf = self;
  if (node->is_url()) {
    actionProvider = ^(NSArray<UIMenuElement*>* suggestedActions) {
      return [weakSelf bookmarkNodeContextualMenuWithIndexPath:indexPath
                                                   canEditNode:canEditNode];
    };
  } else if (node->is_folder()) {
    actionProvider = ^(NSArray<UIMenuElement*>* suggestedActions) {
      return [weakSelf folderNodeContextualMenuWithIndexPath:indexPath
                                                 canEditNode:canEditNode];
    };
  }
  return
      [UIContextMenuConfiguration configurationWithIdentifier:nil
                                              previewProvider:nil
                                               actionProvider:actionProvider];
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForHeaderInSection:(NSInteger)section {
  return [self.tableViewModel numberOfItemsInSection:section] == 0
             ? 0
             : UITableViewAutomaticDimension;
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForFooterInSection:(NSInteger)section {
  // Add space between profile and account sections only if both are not empty,
  // to avoid useless space at the end of the account section content.
  if ([self.tableViewModel sectionIdentifierForSectionIndex:section] ==
          BookmarksHomeSectionIdentifierRootAccount &&
      [self hasItemsInSectionIdentifier:
                BookmarksHomeSectionIdentifierRootLocalOrSyncable] &&
      [self hasItemsInSectionIdentifier:
                BookmarksHomeSectionIdentifierRootAccount]) {
    return kSpaceBetweenAccountAndProfileSections;
  } else {
    return 0;
  }
}

#pragma mark - TableViewURLDragDataSource

- (URLInfo*)tableView:(UITableView*)tableView
    URLInfoAtIndexPath:(NSIndexPath*)indexPath {
  if (indexPath.section !=
      [self.tableViewModel sectionForSectionIdentifier:
                               BookmarksHomeSectionIdentifierBookmarks]) {
    return nil;
  }

  const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
  if (!node || node->is_folder()) {
    return nil;
  }
  return [[URLInfo alloc]
      initWithURL:node->url()
            title:bookmark_utils_ios::TitleForBookmarkNode(node)];
}

#pragma mark - TableViewURLDropDelegate

- (BOOL)canHandleURLDropInTableView:(UITableView*)tableView {
  return !self.mediator.currentlyShowingSearchResults &&
         !self.tableView.hasActiveDrag && ![self isDisplayingBookmarkRoot];
}

- (void)tableView:(UITableView*)tableView
       didDropURL:(const GURL&)URL
      atIndexPath:(NSIndexPath*)indexPath {
  if (!self.displayedFolderNode) {
    // If the view is being closed, let’s not add the URL.
    return;
  }
  NSUInteger index = base::checked_cast<NSUInteger>(indexPath.item);

  [self.snackbarCommandsHandler
      showSnackbarMessage:
          bookmark_utils_ios::CreateBookmarkAtPositionWithUndoSnackbar(
              base::SysUTF8ToNSString(URL.spec()), URL,
              self.displayedFolderNode, index, _bookmarkModel.get(),
              self.profile)];
}

@end
