chromium/ios/chrome/browser/bookmarks/ui_bundled/home/bookmarks_home_view_controller.mm

// 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 <set>

#import "base/apple/foundation_util.h"
#import "base/containers/contains.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/ranges/algorithm.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/strings/grit/components_strings.h"
#import "ios/chrome/app/tests_hook.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/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_ui_constants.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/bookmark_utils_ios.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/cells/bookmark_home_node_item.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/cells/bookmark_table_cell_title_edit_delegate.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/cells/table_view_bookmarks_folder_item.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/folder_chooser/bookmarks_folder_chooser_coordinator.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/folder_chooser/bookmarks_folder_chooser_coordinator_delegate.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/home/bookmarks_coordinator.h"
#import "ios/chrome/browser/bookmarks/ui_bundled/home/bookmarks_coordinator_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/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/intents_donation_helper.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.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/model/browser/browser.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/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/shared/ui/elements/home_waiting_view.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_url_item.h"
#import "ios/chrome/browser/shared/ui/table_view/legacy_chrome_table_view_styler.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/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/ui/authentication/cells/signin_promo_view_configurator.h"
#import "ios/chrome/browser/ui/authentication/cells/table_view_signin_promo_item.h"
#import "ios/chrome/browser/ui/menu/browser_action_factory.h"
#import "ios/chrome/browser/ui/menu/menu_histograms.h"
#import "ios/chrome/browser/ui/sharing/sharing_coordinator.h"
#import "ios/chrome/browser/ui/sharing/sharing_params.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.
};

// Estimated TableView row height.
constexpr CGFloat kEstimatedRowHeight = 65.0;
// 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 () <
    BookmarksCoordinatorDelegate,
    BookmarksFolderChooserCoordinatorDelegate,
    BookmarksHomeConsumer,
    BookmarkModelBridgeObserver,
    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;

// 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, strong) BookmarksCoordinator* bookmarksCoordinator;

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

@synthesize editingFolderCell = _editingFolderCell;

- (instancetype)initWithBrowser:(Browser*)browser {
  DCHECK(browser);

  UITableViewStyle style = ChromeTableViewStyle();
  self = [super initWithStyle:style];
  if (self) {
    _browser = browser->AsWeakPtr();
    ChromeBrowserState* browserState = self.browserState;
    _webStateList = browser->GetWebStateList();

    _faviconLoader =
        IOSChromeFaviconLoaderFactory::GetForBrowserState(browserState);

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

- (void)dealloc {
  DCHECK(_isShutDown);
}

- (void)shutdown {
  _isShutDown = YES;
  [self.editingFolderCell stopEdit];
  [self stopFolderChooserCoordinator];
  [self.bookmarksCoordinator stop];
  self.bookmarksCoordinator = nil;
  [self.mediator disconnect];
  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;
}

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

- (BOOL)canDismiss {
  if (self.folderChooserCoordinator &&
      ![self.folderChooserCoordinator canDismiss]) {
    return NO;
  }
  if (self.bookmarksCoordinator && ![self.bookmarksCoordinator 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());
  DCHECK([self isDisplayingBookmarkRoot]);

  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.browserState->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);
    DCHECK(node);
    // 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];
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  // 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;
  self.navigationItem.hidesSearchBarWhenScrolling = NO;

  self.searchTerm = @"";

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

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

  if (_isShutDown) {
    // After `shutdown` is called, `_browserState` 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];
}

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

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  // Stop edit of current bookmark folder name, if any.
  [self.editingFolderCell stopEdit];
}

- (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("MobileKeyCommandClose"));
  [self navigationBarCancel:nil];
}

#pragma mark - Protected

- (void)loadBookmarkViews {
  DCHECK(self.displayedFolderNode);
  [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.estimatedRowHeight = kEstimatedRowHeight;
  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];

  DCHECK(_bookmarkModel->loaded());
  DCHECK([self isViewLoaded]);
}

- (void)cacheIndexPathRow {
  // Cache IndexPathRow for BookmarkTableView.
  int topMostVisibleIndexPathRow = [self topMostVisibleIndexPathRow];
  if (self.displayedFolderNode) {
    [BookmarkPathCache
        cacheBookmarkTopMostRowWithPrefService:self.browserState->GetPrefs()
                                      folderId:self.displayedFolderNode->id()
                                     inStorage:bookmark_utils_ios::
                                                   GetBookmarkStorageType(
                                                       self.displayedFolderNode,
                                                       _bookmarkModel.get())
                                    topMostRow:topMostVisibleIndexPathRow];
  } else {
    // TODO(crbug.com/40679851):Remove DCHECK once we know the root cause of the
    // bug, for now this will cause a crash on Dev/Canary and we should get
    // breadcrumbs.
    DCHECK(NO);
  }
}

#pragma mark - BookmarksHomeConsumer

- (void)closeThisFolder {
  [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;
}

- (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 {
  UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
  [self loadFaviconAtIndexPath:indexPath
                       forCell:cell
        fallbackToGoogleServer:fallbackToGoogleServer];
}

// Asynchronously loads favicon for given index path. The loads are cancelled
// upon cell reuse automatically.  When the favicon is not found in cache, try
// loading it from a Google server if `fallbackToGoogleServer` is YES,
// otherwise, use the fall back icon style.
- (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath
                       forCell:(UITableViewCell*)cell
        fallbackToGoogleServer:(BOOL)fallbackToGoogleServer {
  const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
  if (node->is_folder()) {
    return;
  }

  // Start loading a favicon.
  __weak BookmarksHomeViewController* weakSelf = self;
  GURL blockURL(node->url());
  auto faviconLoadedBlock = ^(FaviconAttributes* attributes) {
    BookmarksHomeViewController* strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    // Due to search filtering, we also need to validate the indexPath
    // requested versus what is in the table now.
    if (![strongSelf hasItemAtIndexPath:indexPath] ||
        [strongSelf nodeAtIndexPath:indexPath] != node) {
      return;
    }
    TableViewURLCell* URLCell =
        base::apple::ObjCCastStrict<TableViewURLCell>(cell);
    [URLCell.faviconView configureWithAttributes:attributes];
  };

  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 {
  [self.applicationCommandsHandler showSignin:command
                           baseViewController:self.navigationController];
}

- (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 {
  [self ensureBookmarksCoordinator];
  [self.bookmarksCoordinator showAccountSettings];
}

#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:^{
    if ([weakSelf isIncognitoForced]) {
      return;
    }
    [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:^{
        if (![weakSelf isIncognitoAvailable]) {
          return;
        }
        [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:^{
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf editBookmarkNodeWithID:nodeID];
  }];
  [menuElements addObject:editAction];
  // Add share menu item.
  [menuElements addObject:[actionFactory actionToShareWithBlock:^{
                  __strong __typeof(weakSelf) strongSelf = weakSelf;
                  [strongSelf shareURLBookmarkNodeWithID:nodeID
                                               indexPath:indexPath];
                }]];
  // Add delete menu item.
  UIAction* deleteAction = [actionFactory actionToDeleteWithBlock:^{
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf 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:^{
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf editFolderNodeWithID:nodeID];
  }];
  [menuElements addObject:editAction];
  // Add move menu item.
  UIAction* moveAction = [actionFactory actionToMoveFolderWithBlock:^{
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf 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);

  bookmark_utils_ios::NodeSet 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* editedNode = *(nodes.begin());
  const BookmarkNode* selectedFolder = editedNode->parent();
  _folderChooserCoordinator = [[BookmarksFolderChooserCoordinator alloc]
      initWithBaseViewController:self.navigationController
                         browser:_browser.get()
                     hiddenNodes: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::DeleteBookmarksWithUndoToast(
                              nodes, _bookmarkModel.get(), self.browserState,
                              FROM_HERE)];
  [self setTableViewEditing:NO];
}

// Ensures bookmarkInteractionController is set.
- (void)ensureBookmarksCoordinator {
  if (!self.bookmarksCoordinator) {
    self.bookmarksCoordinator =
        [[BookmarksCoordinator alloc] initWithBrowser:_browser.get()];
    self.bookmarksCoordinator.baseViewController = self;
    self.bookmarksCoordinator.delegate = self;
  }
}

// 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 ensureBookmarksCoordinator];
  [self.bookmarksCoordinator presentEditorForURLNode:bookmarkNode];
}

// 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 ensureBookmarksCoordinator];
  [self.bookmarksCoordinator presentEditorForFolderNode:bookmarkNode];
}

- (void)openAllURLs:(std::vector<GURL>)urls
        inIncognito:(BOOL)inIncognito
             newTab:(BOOL)newTab {
  if (inIncognito) {
    IncognitoReauthSceneAgent* reauthAgent = [IncognitoReauthSceneAgent
        agentFromScene:_browser.get()->GetSceneState()];
    if (reauthAgent.authenticationRequired) {
      __weak BookmarksHomeViewController* weakSelf = self;
      [reauthAgent
          authenticateIncognitoContentWithCompletionBlock:^(BOOL success) {
            if (success) {
              [weakSelf openAllURLs:urls inIncognito:inIncognito newTab:newTab];
            }
          }];
      return;
    }
  }

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

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

    // Create the cancel button.
    [strongSelf.actionSheetCoordinator
        addItemWithTitle:l10n_util::GetNSString(
                             IDS_IOS_BOOKMARKS_HOME_BULK_UPLOAD_ALERT_CANCEL)
                  action:^{
                    base::RecordAction(base::UserMetricsAction(
                        "MobileBookmarksManagerBulkSaveBookmarksToAccountDialog"
                        "Cancelled"));
                    [weakSelf dismissActionSheetCoordinator];
                  }
                   style:UIAlertActionStyleCancel];

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

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

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

  [self refreshContents];

  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 - 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 {
  if (!self.mediator.currentlyShowingSearchResults) {
    BookmarksHomeViewController* controller =
        [self createControllerWithDisplayedFolderNode:folder];
    [self.navigationController pushViewController:controller animated:YES];
    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.browserState->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 = ^{
    NSArray<__kindof UIViewController*>* previousStack =
        weakSelf.navigationController.viewControllers;
    [weakSelf.navigationController setViewControllers:stack animated:YES];
    for (UIViewController* controller in previousStack) {
      BookmarksHomeViewController* bookmarksHomeViewController =
          base::apple::ObjCCastStrict<BookmarksHomeViewController>(controller);
      [bookmarksHomeViewController shutdown];
    }
  };

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

- (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 {
      DCHECK_EQ(node->type(), BookmarkNode::FOLDER);
      [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_IN_MIGRATION();
}

- (void)handleMoveNode:(const BookmarkNode*)node toPosition:(size_t)position {
  [self.snackbarCommandsHandler
      showSnackbarMessage:
          bookmark_utils_ios::UpdateBookmarkPositionWithUndoToast(
              node, self.displayedFolderNode, position, _bookmarkModel.get(),
              self.browserState)];
}

- (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);
}

#pragma mark - BookmarkTableCellTitleEditDelegate

- (void)textDidChangeTo:(NSString*)newName {
  DCHECK(self.mediator.editingFolderNode);
  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 {
  DCHECK(_folderChooserCoordinator);
  DCHECK(folder);

  // Copy the list of edited nodes from BookmarksFolderChooserCoordinator
  // as the reference may become invalid when `_folderChooserCoordinator`
  // is set to nil (if `self` holds the last reference to the object).
  std::set<const BookmarkNode*> editedNodesSet =
      _folderChooserCoordinator.editedNodes;
  // TODO(crbug.com/40268466): Change the type of `editedNodes` to std::vector.
  std::vector<const BookmarkNode*> editedNodesVector(editedNodesSet.begin(),
                                                     editedNodesSet.end());
  [self stopFolderChooserCoordinator];

  DCHECK(!folder->is_url());
  DCHECK_GE(editedNodesVector.size(), 1u);

  [self setTableViewEditing:NO];
  ChromeBrowserState* browserState = self.browserState;
  [self.snackbarCommandsHandler
      showSnackbarMessage:bookmark_utils_ios::MoveBookmarksWithUndoToast(
                              editedNodesVector, _bookmarkModel.get(), folder,
                              browserState,
                              AuthenticationServiceFactory::GetForBrowserState(
                                  browserState)
                                  ->GetWeakPtr(),
                              SyncServiceFactory::GetForBrowserState(
                                  browserState))];
}

- (void)bookmarksFolderChooserCoordinatorDidCancel:
    (BookmarksFolderChooserCoordinator*)coordinator {
  DCHECK(_folderChooserCoordinator);
  [self stopFolderChooserCoordinator];
  [self setTableViewEditing:NO];
}

#pragma mark - BookmarksCoordinatorDelegate

- (void)bookmarksCoordinatorWillCommitTitleOrURLChange:
    (BookmarksCoordinator*)coordinator {
  [self setTableViewEditing:NO];
}

#pragma mark - BookmarkModelBridgeObserver

- (void)bookmarkModelLoaded {
  DCHECK(!self.displayedFolderNode);
  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.browserState->GetPrefs()
                                   bookmarkModel:_bookmarkModel.get()
                                        folderId:&unusedFolderId
                                      topMostRow:&unusedIndexPathRow]) {
    self.isReconstructingFromCache = YES;
  }

  DCHECK(self.spinnerView);
  __weak BookmarksHomeViewController* weakSelf = self;
  [self.spinnerView stopWaitingWithCompletion:^{
    // Early return if the controller has been deallocated.
    BookmarksHomeViewController* strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    [UIView animateWithDuration:0.2f
        animations:^{
          weakSelf.spinnerView.alpha = 0.0;
        }
        completion:^(BOOL finished) {
          BookmarksHomeViewController* innerStrongSelf = weakSelf;
          if (!innerStrongSelf) {
            return;
          }

          // 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 (innerStrongSelf.tableView.backgroundView ==
              innerStrongSelf.spinnerView) {
            innerStrongSelf.tableView.backgroundView = nil;
          }
          innerStrongSelf.spinnerView = nil;
        }];
    [strongSelf loadBookmarkViews];
    [strongSelf.tableView reloadData];
  }];
}

- (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 - Accessibility

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

#pragma mark - private

// Returns the browser state.
- (ChromeBrowserState*)browserState {
  if (Browser* browser = _browser.get()) {
    return browser->GetBrowserState()->GetOriginalChromeBrowserState();
  }
  return nullptr;
}

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

// Stops the folder chooser coordinator.
- (void)stopFolderChooserCoordinator {
  [_folderChooserCoordinator stop];
  _folderChooserCoordinator.delegate = nil;
  _folderChooserCoordinator = 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];
  DCHECK(replacementViewControllers);
  [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 {
  viewController.navigationItem.leftBarButtonItem.action = @selector(back);
  // 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);

  // Add custom done button.
  viewController.navigationItem.rightBarButtonItem =
      [self customizedDoneButton];
}

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

- (UIBarButtonItem*)customizedDoneButton {
  UIBarButtonItem* doneButton = [[UIBarButtonItem alloc]
      initWithTitle:GetNSString(IDS_IOS_NAVIGATION_BAR_DONE_BUTTON)
              style:UIBarButtonItemStyleDone
             target:self
             action:@selector(navigationBarCancel:)];
  doneButton.accessibilityLabel =
      GetNSString(IDS_IOS_NAVIGATION_BAR_DONE_BUTTON);
  doneButton.accessibilityIdentifier =
      kBookmarksHomeNavigationBarDoneButtonIdentifier;
  return doneButton;
}

// 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.browserState->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);

  BookmarksHomeNodeItem* nodeItem = [[BookmarksHomeNodeItem alloc]
      initWithType:BookmarksHomeItemTypeBookmark
      bookmarkNode:self.mediator.editingFolderNode];
  nodeItem.shouldDisplayCloudSlashIcon = [self.mediator
      shouldDisplayCloudSlashIconWithBookmarkNode:self.mediator.displayedNode];
  [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.applicationCommandsHandler = self.applicationCommandsHandler;
  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 {
  // 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;
    if (base::Contains(self.mediator.selectedNodesForEditMode, 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;
  }

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

  // Return the first visible row.
  NSIndexPath* topMostIndexPath = [visibleIndexPaths objectAtIndex:0];
  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 {
  // 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::GetForBrowserState(self.browserState);
  return managedBookmarkService ? !managedBookmarkService->IsNodeManaged(node)
                                : YES;
}

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

// Returns the bookmark node associated with `indexPath`.
- (const BookmarkNode*)nodeAtIndexPath:(NSIndexPath*)indexPath {
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];

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

  DUMP_WILL_BE_NOTREACHED() << "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.
    base::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 (base::Contains(self.mediator.selectedNodesForEditMode, 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;
  self.scrimView.alpha = 0.0f;
  [self.tableView addSubview:self.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.
  AddSameConstraints(self.scrimView, self.view.superview);
  self.tableView.accessibilityElementsHidden = YES;
  self.tableView.scrollEnabled = NO;
  __weak BookmarksHomeViewController* weakSelf = self;
  [UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
                   animations:^{
                     BookmarksHomeViewController* strongSelf = weakSelf;
                     if (!strongSelf) {
                       return;
                     }
                     strongSelf.scrimView.alpha = 1.0f;
                     [strongSelf.view layoutIfNeeded];
                   }];
}

// Hide scrim and restore toolbar.
- (void)hideScrim {
  __weak BookmarksHomeViewController* weakSelf = self;
  [UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
      animations:^{
        weakSelf.scrimView.alpha = 0.0f;
      }
      completion:^(BOOL finished) {
        BookmarksHomeViewController* strongSelf = weakSelf;
        if (!strongSelf) {
          return;
        }
        [strongSelf.scrimView removeFromSuperview];
        strongSelf.tableView.accessibilityElementsHidden = NO;
        strongSelf.tableView.scrollEnabled = YES;
      }];
  [self setupContextBar];
}

- (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 =
      [[SharingCoordinator alloc] initWithBaseViewController:self
                                                     browser:_browser.get()
                                                      params:params
                                                  originView:cellView];
  [self.sharingCoordinator start];
}

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

// Returns whether the incognito mode is available.
- (BOOL)isIncognitoAvailable {
  return !IsIncognitoModeDisabled(self.browserState->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) {
            BookmarksHomeViewController* strongSelf = weakSelf;
            if (!strongSelf) {
              return;
            }
            // 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 (strongSelf.tableView.backgroundView == strongSelf.spinnerView) {
              strongSelf.tableView.backgroundView = nil;
            }
            strongSelf.spinnerView = nil;
          }];
    }];
  }
}

// Shows empty bookmarks background view.
- (void)showEmptyBackground {
  if (!self.emptyViewBackground) {
    self.emptyViewBackground = [[TableViewIllustratedEmptyView alloc]
        initWithFrame:self.tableView.bounds
                image:[UIImage imageNamed:@"bookmark_empty"]
                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_IN_MIGRATION();
      break;
    case BookmarksContextBarSingleURLSelection:
    case BookmarksContextBarMultipleURLSelection:
    case BookmarksContextBarSingleFolderSelection:
    case BookmarksContextBarMultipleFolderSelection:
    case BookmarksContextBarMixedSelection:
      // Delete clicked.
      [self deleteBookmarkNodes:nodes
                     userAction:"MobileBookmarkManagerRemoveSelected"];
      break;
    case BookmarksContextBarNone:
    default:
      NOTREACHED_IN_MIGRATION();
  }
}

// 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_IN_MIGRATION();
      break;
  }

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

// Called when the trailing button, "Select" or "Cancel" 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 (!_externalBookmark) {
    return;
  }

  [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 == _externalBookmark) {
      [self.tableView selectRowAtIndexPath:path
                                  animated:NO
                            scrollPosition:UITableViewScrollPositionMiddle];
      [self.tableView.delegate tableView:self.tableView
                 didSelectRowAtIndexPath:path];
      break;
    }
  }
}

#pragma mark - ContextBarStates

// Customizes the context bar buttons based the `state` passed in.
- (void)setContextBarState:(BookmarksContextBarState)state {
  _contextBarState = state;
  switch (state) {
    case BookmarksContextBarDefault:
      [self setBookmarksContextBarButtonsDefaultState];
      break;
    case BookmarksContextBarBeginSelection:
      [self setBookmarksContextBarSelectionStartState];
      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.moreButton.enabled = YES;
      self.deleteButton.enabled = YES;
      break;
    case BookmarksContextBarNone:
    default:
      break;
  }
}

- (void)setBookmarksContextBarButtonsDefaultState {
  // Set New Folder button
  NSString* titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_NEW_FOLDER);
  UIBarButtonItem* newFolderButton =
      [[UIBarButtonItem alloc] initWithTitle:titleString
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:@selector(leadingButtonClicked)];
  newFolderButton.accessibilityIdentifier =
      kBookmarksHomeLeadingButtonIdentifier;
  newFolderButton.enabled = [self allowsNewFolder];

  // Spacer button.
  UIBarButtonItem* spaceButton = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                           target:nil
                           action:nil];

  // Set Edit button.
  titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_EDIT);
  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];

  [self setToolbarItems:@[ newFolderButton, spaceButton, editButton ]
               animated:NO];
}

- (void)setBookmarksContextBarSelectionStartState {
  // Disabled Delete button.
  NSString* titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_DELETE);
  self.deleteButton =
      [[UIBarButtonItem alloc] initWithTitle:titleString
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:@selector(leadingButtonClicked)];
  self.deleteButton.tintColor = [UIColor colorNamed:kRedColor];
  self.deleteButton.enabled = NO;
  self.deleteButton.accessibilityIdentifier =
      kBookmarksHomeLeadingButtonIdentifier;

  // Disabled More button.
  titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_BAR_MORE);
  self.moreButton =
      [[UIBarButtonItem alloc] initWithTitle:titleString
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:@selector(centerButtonClicked)];
  self.moreButton.enabled = NO;
  self.moreButton.accessibilityIdentifier =
      kBookmarksHomeCenterButtonIdentifier;

  // Enabled Cancel button.
  titleString = GetNSString(IDS_CANCEL);
  UIBarButtonItem* cancelButton =
      [[UIBarButtonItem alloc] initWithTitle:titleString
                                       style:UIBarButtonItemStylePlain
                                      target:self
                                      action:@selector(trailingButtonClicked)];
  cancelButton.accessibilityIdentifier = kBookmarksHomeTrailingButtonIdentifier;

  // Spacer button.
  UIBarButtonItem* spaceButton = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
                           target:nil
                           action:nil];

  [self setToolbarItems:@[
    self.deleteButton, spaceButton, self.moreButton, spaceButton, cancelButton
  ]
               animated:NO];
}

#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 dismissActionSheetCoordinator];
                  BookmarksHomeViewController* strongSelf = weakSelf;
                  if (!strongSelf) {
                    return;
                  }
                  if ([strongSelf isIncognitoForced]) {
                    return;
                  }
                  std::vector<const BookmarkNode*> selectedNodesForEditMode =
                      [strongSelf selectedNodesForEditMode];
                  [strongSelf
                      openAllURLs:GetUrlsToOpen(selectedNodesForEditMode)
                      inIncognito:NO
                           newTab:NO];
                }
                 style:UIAlertActionStyleDefault
               enabled:![self isIncognitoForced]];

  titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_OPEN_INCOGNITO);
  [coordinator
      addItemWithTitle:titleString
                action:^{
                  [weakSelf dismissActionSheetCoordinator];
                  BookmarksHomeViewController* strongSelf = weakSelf;
                  if (!strongSelf) {
                    return;
                  }
                  if (![strongSelf isIncognitoAvailable]) {
                    return;
                  }
                  std::vector<const BookmarkNode*> selectedNodesForEditMode =
                      [strongSelf selectedNodesForEditMode];
                  [strongSelf
                      openAllURLs:GetUrlsToOpen(selectedNodesForEditMode)
                      inIncognito:YES
                           newTab:NO];
                }
                 style:UIAlertActionStyleDefault
               enabled:[self isIncognitoAvailable]];

  const BookmarkNodeIDSet nodeIDs = GetBookmarkNodeIDSet(nodes);

  titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE);
  [coordinator
      addItemWithTitle:titleString
                action:^{
                  [weakSelf dismissActionSheetCoordinator];
                  BookmarksHomeViewController* strongSelf = weakSelf;
                  [strongSelf
                      moveBookmarkNodeWithIDs:nodeIDs
                                   userAction:"MobileBookmarkManagerMove"
                                              "ToFolderBulk"];
                }
                 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 dismissActionSheetCoordinator];
                           BookmarksHomeViewController* strongSelf = weakSelf;
                           [strongSelf editBookmarkNodeWithID:nodeID];
                         }
                          style:UIAlertActionStyleDefault
                        enabled:editEnabled];

  titleString = GetNSString(IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB);
  [coordinator addItemWithTitle:titleString
                         action:^{
                           [weakSelf dismissActionSheetCoordinator];
                           if ([weakSelf isIncognitoForced]) {
                             return;
                           }
                           [weakSelf openAllURLs:{nodeURL}
                                     inIncognito:NO
                                          newTab:YES];
                         }
                          style:UIAlertActionStyleDefault
                        enabled:![self isIncognitoForced]];

  if (base::ios::IsMultipleScenesSupported()) {
    titleString = GetNSString(IDS_IOS_CONTENT_CONTEXT_OPENINNEWWINDOW);
    auto action = ^{
      [weakSelf dismissActionSheetCoordinator];
      [weakSelf.applicationCommandsHandler
          openNewWindowWithActivity:ActivityToLoadURL(
                                        WindowActivityBookmarksOrigin,
                                        nodeURL)];
    };
    [coordinator addItemWithTitle:titleString
                           action:action
                            style:UIAlertActionStyleDefault];
  }

  titleString = GetNSString(IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB);
  [coordinator addItemWithTitle:titleString
                         action:^{
                           [weakSelf dismissActionSheetCoordinator];
                           if (![weakSelf isIncognitoAvailable]) {
                             return;
                           }
                           [weakSelf openAllURLs:{nodeURL}
                                     inIncognito:YES
                                          newTab:YES];
                         }
                          style:UIAlertActionStyleDefault
                        enabled:[self isIncognitoAvailable]];

  titleString = GetNSString(IDS_IOS_CONTENT_CONTEXT_COPY);
  [coordinator
      addItemWithTitle:titleString
                action:^{
                  [weakSelf dismissActionSheetCoordinator];
                  // Use strongSelf even though the object is only used once
                  // because we do not want to change the global pasteboard
                  // if the view has been deallocated.
                  BookmarksHomeViewController* strongSelf = weakSelf;
                  if (!strongSelf) {
                    return;
                  }
                  [strongSelf setTableViewEditing:NO];
                  StoreTextInPasteboard(base::SysUTF8ToNSString(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 dismissActionSheetCoordinator];
                           BookmarksHomeViewController* strongSelf = weakSelf;
                           [strongSelf editFolderNodeWithID:nodeID];
                         }
                          style:UIAlertActionStyleDefault
                        enabled:editEnabled];

  titleString = GetNSString(IDS_IOS_BOOKMARK_CONTEXT_MENU_MOVE);
  [coordinator
      addItemWithTitle:titleString
                action:^{
                  [weakSelf dismissActionSheetCoordinator];
                  BookmarksHomeViewController* strongSelf = weakSelf;
                  [strongSelf
                      moveBookmarkNodeWithIDs:{nodeID}
                                   userAction:"MobileBookmarkManagerMove"
                                              "ToFolder"];
                }
                 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 dismissActionSheetCoordinator];
                  BookmarksHomeViewController* strongSelf = weakSelf;
                  [strongSelf
                      moveBookmarkNodeWithIDs:nodeIDs
                                   userAction:"MobileBookmarkManagerMove"
                                              "ToFolderBulk"];
                }
                 style:UIAlertActionStyleDefault];
}

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

#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 (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_IN_MIGRATION();
    return;
  }

  [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 {
  DCHECK_EQ(self.searchController, searchController);
  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 {
  UITableViewCell* cell = [super tableView:tableView
                     cellForRowAtIndexPath:indexPath];
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];

  cell.userInteractionEnabled =
      (item.type != BookmarksHomeItemTypeMessage &&
       item.type != BookmarksHomeItemTypeBatchUploadRecommendation);
  if (item.type == BookmarksHomeItemTypeBatchUploadRecommendation) {
    cell.separatorInset =
        UIEdgeInsetsMake(0.f, kTableViewSeparatorInset, 0.f, 0.f);
  }

  if (item.type == BookmarksHomeItemTypeBookmark) {
    BookmarksHomeNodeItem* nodeItem =
        base::apple::ObjCCastStrict<BookmarksHomeNodeItem>(item);
    if (nodeItem.bookmarkNode->is_folder() &&
        nodeItem.bookmarkNode == 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(), ^{
        BookmarksHomeViewController* strongSelf = weakSelf;
        if (!strongSelf) {
          return;
        }
        strongSelf.editingFolderCell = tableCell;
        [tableCell startEdit];
        tableCell.textDelegate = strongSelf;
      });
    }

    // Load the favicon from cache. If not found, try fetching it from a
    // Google Server.
    [self loadFaviconAtIndexPath:indexPath
                         forCell:cell
          fallbackToGoogleServer:YES];
  }

  return cell;
}

- (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath {
  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;
  return [self isEditBookmarksEnabled] && [self isUrlOrFolder:node] &&
         [self isNodeEditableByUser:node];
}

- (void)tableView:(UITableView*)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
     forRowAtIndexPath:(NSIndexPath*)indexPath {
  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;
    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) {
    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;
  [self handleMoveNode:node toPosition:newPosition];
}

#pragma mark - UITableViewDelegate

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

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  BookmarksHomeSectionIdentifier sectionIdentifier =
      (BookmarksHomeSectionIdentifier)([self.tableViewModel
          sectionIdentifierForSectionIndex:indexPath.section]);
  if (IsABookmarkNodeSectionForIdentifier(sectionIdentifier)) {
    const BookmarkNode* node = [self nodeAtIndexPath:indexPath];
    DCHECK(node);
    // 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];
    DCHECK(node);
    self.mediator.selectedNodesForEditMode.erase(node);
    [self handleSelectEditNodes:self.mediator.selectedNodesForEditMode];
  }
}

- (UIContextMenuConfiguration*)tableView:(UITableView*)tableView
    contextMenuConfigurationForRowAtIndexPath:(NSIndexPath*)indexPath
                                        point:(CGPoint)point {
  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 {
  NSUInteger index = base::checked_cast<NSUInteger>(indexPath.item);

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

@end