chromium/ios/chrome/browser/ui/reading_list/reading_list_table_view_controller.mm

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/reading_list/reading_list_table_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/check_op.h"
#import "base/ios/ios_util.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/app/tests_hook.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/intents/intents_donation_helper.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/list_model/list_item+Controller.h"
#import "ios/chrome/browser/shared/ui/list_model/list_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_link_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_switch_cell.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/authentication/cells/signin_promo_view_configurator.h"
#import "ios/chrome/browser/ui/authentication/cells/signin_promo_view_delegate.h"
#import "ios/chrome/browser/ui/authentication/cells/table_view_signin_promo_item.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_constants.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_data_sink.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_data_source.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_list_item.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_list_item_updater.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_list_view_controller_audience.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_list_view_controller_delegate.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_menu_provider.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_toolbar_button_commands.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_toolbar_button_manager.h"
#import "ios/chrome/browser/ui/settings/cells/sync_switch_item.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/strings/grit/ui_strings.h"

namespace {

// Height for the header on top of the sign-in promo cell.
constexpr CGFloat kSignInPromoSectionHeaderHeight = 10;

// Types of ListItems used by the reading list UI.
enum ReadingListItemType {
  kItemTypeHeader = kItemTypeEnumZero,
  kItemTypeItem,
  kSwitchItemType,
  kSwitchItemFooterType,
  kItemTypeSignInPromo,
};
// Identifiers for sections in the reading list UI.
enum ReadingListSectionIdentifier {
  kSectionIdentifierSignInPromo = kSectionIdentifierEnumZero,
  kSectionIdentifierUnread,
  kSectionIdentifierRead,
};

// Returns the ReadingListSelectionState corresponding with the provided numbers
// of read and unread items.
ReadingListSelectionState GetSelectionStateForSelectedCounts(
    NSUInteger selected_unread_count,
    NSUInteger selected_read_count) {
  if (selected_read_count > 0 && selected_unread_count > 0)
    return ReadingListSelectionState::READ_AND_UNREAD_ITEMS;
  if (selected_read_count > 0)
    return ReadingListSelectionState::ONLY_READ_ITEMS;
  if (selected_unread_count > 0)
    return ReadingListSelectionState::ONLY_UNREAD_ITEMS;
  return ReadingListSelectionState::NONE;
}

}  // namespace

@interface ReadingListTableViewController () <ReadingListDataSink,
                                              ReadingListToolbarButtonCommands,
                                              TableViewURLDragDataSource>

// Redefine the model to return ReadingListListItems
@property(nonatomic, readonly) TableViewModel<TableViewItem*>* tableViewModel;

// The number of batch operation triggered by UI.
// One UI operation can trigger multiple batch operation, so this can be greater
// than 1.
@property(nonatomic, assign) int numberOfBatchOperationInProgress;
// Whether the data source has been modified while in editing mode.
@property(nonatomic, assign) BOOL dataSourceModifiedWhileEditing;
// The toolbar button manager.
@property(nonatomic, strong) ReadingListToolbarButtonManager* toolbarManager;
// The number of read and unread cells that are currently selected.
@property(nonatomic, assign) NSUInteger selectedUnreadItemCount;
@property(nonatomic, assign) NSUInteger selectedReadItemCount;
// The action sheet used to confirm whether items should be marked as read or
// unread.
@property(nonatomic, strong) ActionSheetCoordinator* markConfirmationSheet;
// Whether the table view is being edited after tapping on the edit button in
// the toolbar.
@property(nonatomic, assign, getter=isEditingWithToolbarButtons)
    BOOL editingWithToolbarButtons;
// Whether the table view is being edited by the swipe-to-delete button.
@property(nonatomic, readonly, getter=isEditingWithSwipe) BOOL editingWithSwipe;
// Handler for URL drag interactions.
@property(nonatomic, strong) TableViewURLDragDropHandler* dragDropHandler;
@end

@implementation ReadingListTableViewController
@dynamic tableViewModel;

- (instancetype)init {
  UITableViewStyle style = ChromeTableViewStyle();
  self = [super initWithStyle:style];
  if (self) {
    _toolbarManager = [[ReadingListToolbarButtonManager alloc] init];
    _toolbarManager.commandHandler = self;
  }
  return self;
}

#pragma mark - Accessors

- (void)setAudience:(id<ReadingListListViewControllerAudience>)audience {
  if (_audience == audience)
    return;
  _audience = audience;
  BOOL hasItems = self.dataSource.ready && self.dataSource.hasElements;
  [_audience readingListHasItems:hasItems];
}

- (void)setDataSource:(id<ReadingListDataSource>)dataSource {
  DCHECK(self.browser);
  if (_dataSource == dataSource)
    return;
  _dataSource.dataSink = nil;
  _dataSource = dataSource;
  _dataSource.dataSink = self;
}

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
  if (self.editing == editing)
    return;
  [super setEditing:editing animated:animated];
  self.selectedUnreadItemCount = 0;
  self.selectedReadItemCount = 0;
  if (!editing) {
    [self reloadDataIfNeededAndNotEditing];
    self.markConfirmationSheet = nil;
    self.editingWithToolbarButtons = NO;
    [self removeEmptySections];
  }
  [self updateToolbarItems];

  // Force update a11y actions based on edit mode.
  for (int section = 0; section < self.tableViewModel.numberOfSections;
       section++) {
    if (![self.tableViewModel numberOfItemsInSection:section]) {
      continue;
    }
    NSInteger sectionIdentifier =
        [self.tableViewModel sectionIdentifierForSectionIndex:section];
    [self reconfigureCellsForItems:
              [self.tableViewModel
                  itemsInSectionWithIdentifier:sectionIdentifier]];
  }
}

- (void)setSelectedUnreadItemCount:(NSUInteger)selectedUnreadItemCount {
  if (_selectedUnreadItemCount == selectedUnreadItemCount)
    return;
  BOOL hadSelectedUnreadItems = _selectedUnreadItemCount > 0;
  _selectedUnreadItemCount = selectedUnreadItemCount;
  if ((_selectedUnreadItemCount > 0) != hadSelectedUnreadItems)
    [self updateToolbarItems];
}

- (void)setSelectedReadItemCount:(NSUInteger)selectedReadItemCount {
  if (_selectedReadItemCount == selectedReadItemCount)
    return;
  BOOL hadSelectedReadItems = _selectedReadItemCount > 0;
  _selectedReadItemCount = selectedReadItemCount;
  if ((_selectedReadItemCount > 0) != hadSelectedReadItems)
    [self updateToolbarItems];
}

- (void)setMarkConfirmationSheet:
    (ActionSheetCoordinator*)markConfirmationSheet {
  if (_markConfirmationSheet == markConfirmationSheet)
    return;
  [_markConfirmationSheet stop];
  _markConfirmationSheet = markConfirmationSheet;
}

- (BOOL)isEditingWithSwipe {
  return self.editing && !self.editingWithToolbarButtons;
}

#pragma mark - Public

- (void)reloadData {
  [self.tableView.contextMenuInteraction dismissMenu];
  [self loadModel];
  if (self.viewLoaded)
    [self.tableView reloadData];
}

- (void)willBeDismissed {
  [self.dataSource dataSinkWillBeDismissed];
  [self dismissMarkConfirmationSheet];
}

+ (NSString*)accessibilityIdentifier {
  return kReadingListViewID;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  self.title = l10n_util::GetNSString(IDS_IOS_TOOLS_MENU_READING_LIST);

  self.tableView.accessibilityIdentifier =
      [[self class] accessibilityIdentifier];
  self.tableView.estimatedRowHeight = 56;
  self.tableView.rowHeight = UITableViewAutomaticDimension;
  self.tableView.allowsMultipleSelectionDuringEditing = YES;
  self.tableView.allowsMultipleSelection = YES;
  self.dragDropHandler = [[TableViewURLDragDropHandler alloc] init];
  self.dragDropHandler.origin = WindowActivityReadingListOrigin;
  self.dragDropHandler.dragDataSource = self;
  self.tableView.dragDelegate = self.dragDropHandler;
  self.tableView.dragInteractionEnabled = true;
  self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
}

- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
  // In case the sign-in promo visibility is changed before the first layout,
  // we need to refresh the empty view margin after the layout is done, to apply
  // the correct top margin value according to the promo view's height.
  [self updateEmptyViewTopMargin];
  [IntentDonationHelper donateIntent:IntentType::kOpenReadingList];
}

- (void)viewWillTransitionToSize:(CGSize)size
       withTransitionCoordinator:
           (id<UIViewControllerTransitionCoordinator>)coordinator {
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
  if (self.editingWithSwipe)
    [self exitEditingModeAnimated:YES];
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];
  if (!self.dataSource.hasElements &&
      self.traitCollection.preferredContentSizeCategory !=
          previousTraitCollection.preferredContentSizeCategory) {
    [self tableIsEmpty];
  }
}

#pragma mark - UITableViewDataSource

- (void)tableView:(UITableView*)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
     forRowAtIndexPath:(NSIndexPath*)indexPath {
  DCHECK_EQ(editingStyle, UITableViewCellEditingStyleDelete);
  base::RecordAction(base::UserMetricsAction("MobileReadingListDeleteEntry"));

  [self deleteItemsAtIndexPaths:@[ indexPath ]
                     endEditing:NO
            removeEmptySections:NO];
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  if (self.editing) {
    // Update the selected item counts and the toolbar buttons.
    ReadingListSectionIdentifier sectionID =
        static_cast<ReadingListSectionIdentifier>([self.tableViewModel
            sectionIdentifierForSectionIndex:indexPath.section]);
    switch (sectionID) {
      case kSectionIdentifierUnread:
        self.selectedUnreadItemCount++;
        break;
      case kSectionIdentifierRead:
        self.selectedReadItemCount++;
        break;
      case kSectionIdentifierSignInPromo:
        NOTREACHED();
    }
  } else {
    // Open the URL.
    TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
    // TODO(crbug.com/40263259): the runtime check will be replaced using new
    // methods implementations in TableViewItem and ReadingListTableViewItem.
    if ([item conformsToProtocol:@protocol(ReadingListListItem)]) {
      [self.delegate
          readingListListViewController:self
                               openItem:(id<ReadingListListItem>)item];
    }
  }
}

- (void)tableView:(UITableView*)tableView
    didDeselectRowAtIndexPath:(NSIndexPath*)indexPath {
  if (self.editing) {
    // Update the selected item counts and the toolbar buttons.
    ReadingListSectionIdentifier sectionID =
        static_cast<ReadingListSectionIdentifier>([self.tableViewModel
            sectionIdentifierForSectionIndex:indexPath.section]);
    switch (sectionID) {
      case kSectionIdentifierUnread:
        self.selectedUnreadItemCount--;
        break;
      case kSectionIdentifierRead:
        self.selectedReadItemCount--;
        break;
      case kSectionIdentifierSignInPromo:
        NOTREACHED();
    }
  }
}

- (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath {
  return [self.tableViewModel itemAtIndexPath:indexPath].type == kItemTypeItem;
}

- (UIContextMenuConfiguration*)tableView:(UITableView*)tableView
    contextMenuConfigurationForRowAtIndexPath:(NSIndexPath*)indexPath
                                        point:(CGPoint)point {
  if (self.isEditing) {
    // Don't show the context menu when currently in editing mode.
    return nil;
  }
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
  // TODO(crbug.com/40263259): the runtime check will be replaced using new
  // methods implementations in TableViewItem and ReadingListTableViewItem.
  if ([item conformsToProtocol:@protocol(ReadingListListItem)]) {
    return [self.menuProvider
        contextMenuConfigurationForItem:(id<ReadingListListItem>)item
                               withView:[self.tableView
                                            cellForRowAtIndexPath:indexPath]];
  } else {
    return nil;
  }
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForHeaderInSection:(NSInteger)section {
  if ([self.tableViewModel sectionIdentifierForSectionIndex:section] ==
      kSectionIdentifierSignInPromo) {
    return kSignInPromoSectionHeaderHeight;
  }
  return UITableViewAutomaticDimension;
}

#pragma mark - TableViewURLDragDataSource

- (URLInfo*)tableView:(UITableView*)tableView
    URLInfoAtIndexPath:(NSIndexPath*)indexPath {
  if (self.tableView.editing)
    return nil;
  TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
  // TODO(crbug.com/40263259): the runtime check will be replaced using new
  // methods implementations in TableViewItem and ReadingListTableViewItem.
  if ([item conformsToProtocol:@protocol(ReadingListListItem)]) {
    id<ReadingListListItem> readingListItem = (id<ReadingListListItem>)item;
    return [[URLInfo alloc] initWithURL:readingListItem.entryURL
                                  title:readingListItem.title];
  } else {
    return nil;
  }
}

#pragma mark - UIAdaptivePresentationControllerDelegate

- (void)presentationControllerDidDismiss:
    (UIPresentationController*)presentationController {
  base::RecordAction(base::UserMetricsAction("IOSReadingListCloseWithSwipe"));
  // Call the delegate dismissReadingListListViewController to clean up state
  // and stop the Coordinator.
  [self.delegate dismissReadingListListViewController:self];
}

#pragma mark - LegacyChromeTableViewController

- (void)loadModel {
  [super loadModel];
  self.dataSourceModifiedWhileEditing = NO;
  if (self.dataSource.hasElements) {
    [self tableIsNotEmpty];
  } else {
    [self tableIsEmpty];
  }
  [self.delegate didLoadContent];
}

#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*)keyCommands {
  return @[ UIKeyCommand.cr_close ];
}

- (void)keyCommand_close {
  base::RecordAction(base::UserMetricsAction("MobileKeyCommandClose"));
  [self.delegate dismissReadingListListViewController:self];
}

#pragma mark - ReadingListDataSink

- (void)dataSourceReady:(id<ReadingListDataSource>)dataSource {
  [self reloadData];
}

- (void)dataSourceChanged {
  // If the model is updated when the UI is already making a change, set a flag
  // to reload the data at the end of the editing.
  if (self.numberOfBatchOperationInProgress || self.isEditing) {
    self.dataSourceModifiedWhileEditing = YES;
  } else {
    [self reloadData];
  }
}

- (NSArray<id<ReadingListListItem>>*)readItems {
  return [self itemsForSection:kSectionIdentifierRead];
}

- (NSArray<id<ReadingListListItem>>*)unreadItems {
  return [self itemsForSection:kSectionIdentifierUnread];
}

- (void)itemHasChangedAfterDelay:(id<ReadingListListItem>)item {
  TableViewItem<ReadingListListItem>* tableItem =
      [self tableItemForReadingListItem:item];
  if ([self.tableViewModel hasItem:tableItem])
    [self reconfigureCellsForItems:@[ tableItem ]];
}

- (void)itemsHaveChanged:(NSArray<ListItem*>*)items {
  [self reconfigureCellsForItems:items];
}

#pragma mark - ReadingListDataSink Helpers

// Returns the items for the `sectionID`.
- (NSArray<id<ReadingListListItem>>*)itemsForSection:
    (ReadingListSectionIdentifier)sectionID {
  TableViewModel* model = self.tableViewModel;
  return [model hasSectionForSectionIdentifier:sectionID]
             ? [model itemsInSectionWithIdentifier:sectionID]
             : nil;
}

#pragma mark - ReadingListListItemAccessibilityDelegate

- (BOOL)isItemRead:(id<ReadingListListItem>)item {
  return [self.dataSource isItemRead:item];
}

- (void)openItemInNewTab:(id<ReadingListListItem>)item {
  [self.delegate readingListListViewController:self
                              openItemInNewTab:item
                                     incognito:NO];
}

- (void)openItemInNewIncognitoTab:(id<ReadingListListItem>)item {
  [self.delegate readingListListViewController:self
                              openItemInNewTab:item
                                     incognito:YES];
}

- (void)openItemOffline:(id<ReadingListListItem>)item {
  [self.delegate readingListListViewController:self
                       openItemOfflineInNewTab:item];
}

- (void)markItemRead:(id<ReadingListListItem>)item {
  TableViewModel* model = self.tableViewModel;
  if (![model hasSectionForSectionIdentifier:kSectionIdentifierUnread]) {
    // Prevent trying to access this section if it has been concurrently
    // deleted (via another window or Sync).
    return;
  }

  TableViewItem* tableViewItem =
      base::apple::ObjCCastStrict<TableViewItem>(item);
  if ([model hasItem:tableViewItem
          inSectionWithIdentifier:kSectionIdentifierUnread]) {
    [self markItemsAtIndexPaths:@[ [model indexPathForItem:tableViewItem] ]
                 withReadStatus:YES];
  }
}

- (void)markItemUnread:(id<ReadingListListItem>)item {
  TableViewModel* model = self.tableViewModel;
  if (![model hasSectionForSectionIdentifier:kSectionIdentifierRead]) {
    // Prevent trying to access this section if it has been concurrently
    // deleted (via another window or Sync).
    return;
  }

  TableViewItem* tableViewItem =
      base::apple::ObjCCastStrict<TableViewItem>(item);
  if ([model hasItem:tableViewItem
          inSectionWithIdentifier:kSectionIdentifierRead]) {
    [self markItemsAtIndexPaths:@[ [model indexPathForItem:tableViewItem] ]
                 withReadStatus:NO];
  }
}

- (void)deleteItem:(id<ReadingListListItem>)item {
  TableViewItem<ReadingListListItem>* tableViewItem =
      base::apple::ObjCCastStrict<TableViewItem<ReadingListListItem>>(item);
  if ([self.tableViewModel hasItem:tableViewItem]) {
    NSIndexPath* indexPath =
        [self.tableViewModel indexPathForItem:tableViewItem];
    [self deleteItemsAtIndexPaths:@[ indexPath ]];
  }
}

#pragma mark - ReadingListToolbarButtonCommands

- (void)enterReadingListEditMode {
  if (self.editing && !self.editingWithToolbarButtons) {
    // Reset swipe editing to trigger button editing
    [self setEditing:NO animated:NO];
  }
  if (self.editing) {
    return;
  }
  self.editingWithToolbarButtons = YES;
  [self setEditing:YES animated:YES];
}

- (void)exitReadingListEditMode {
  if (!self.editing)
    return;
  [self exitEditingModeAnimated:YES];
}

- (void)deleteAllReadReadingListItems {
  base::RecordAction(base::UserMetricsAction("MobileReadingListDeleteRead"));
  if (![self hasItemInSection:kSectionIdentifierRead]) {
    [self exitEditingModeAnimated:YES];
    return;
  }

  // Delete the items in the data source and exit editing mode.
  ReadingListListItemUpdater updater = ^(id<ReadingListListItem> item) {
    [self.dataSource removeEntryFromItem:item];
  };
  [self updateItemsInSection:kSectionIdentifierRead withItemUpdater:updater];

  // Update the model and table view for the deleted items.
  UITableView* tableView = self.tableView;
  TableViewModel* model = self.tableViewModel;
  void (^updates)(void) = ^{
    NSInteger sectionIndex =
        [model sectionForSectionIdentifier:kSectionIdentifierRead];
    [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
             withRowAnimation:UITableViewRowAnimationMiddle];
    [model removeSectionWithIdentifier:kSectionIdentifierRead];
  };
  void (^completion)(BOOL) = ^(BOOL) {
    [self batchEditDidFinish];
  };
  [self performBatchTableViewUpdates:updates completion:completion];
  [self exitEditingModeAnimated:YES];
}

- (void)deleteSelectedReadingListItems {
  base::RecordAction(
      base::UserMetricsAction("MobileReadingListDeleteSelected"));
  [self deleteItemsAtIndexPaths:self.tableView.indexPathsForSelectedRows];
  [self exitEditingModeAnimated:YES];
}

- (void)markSelectedReadingListItemsRead {
  [self markItemsAtIndexPaths:self.tableView.indexPathsForSelectedRows
               withReadStatus:YES];
}

- (void)markSelectedReadingListItemsUnread {
  [self markItemsAtIndexPaths:self.tableView.indexPathsForSelectedRows
               withReadStatus:NO];
}

- (void)markSelectedReadingListItemsAfterConfirmation {
  [self initializeMarkConfirmationSheet];
  __weak ReadingListTableViewController* weakSelf = self;
  NSArray<NSIndexPath*>* selectedIndexPaths =
      self.tableView.indexPathsForSelectedRows;
  NSString* markAsReadTitle =
      l10n_util::GetNSStringWithFixup(IDS_IOS_READING_LIST_MARK_READ_BUTTON);
  [self.markConfirmationSheet
      addItemWithTitle:markAsReadTitle
                action:^{
                  [weakSelf markItemsAtIndexPaths:selectedIndexPaths
                                   withReadStatus:YES];
                  [weakSelf dismissMarkConfirmationSheet];
                }
                 style:UIAlertActionStyleDefault];
  NSString* markAsUnreadTitle =
      l10n_util::GetNSStringWithFixup(IDS_IOS_READING_LIST_MARK_UNREAD_BUTTON);
  [self.markConfirmationSheet
      addItemWithTitle:markAsUnreadTitle
                action:^{
                  [weakSelf markItemsAtIndexPaths:selectedIndexPaths
                                   withReadStatus:NO];
                  [weakSelf dismissMarkConfirmationSheet];
                }
                 style:UIAlertActionStyleDefault];
  [self.markConfirmationSheet start];
}

- (void)markAllReadingListItemsAfterConfirmation {
  [self initializeMarkConfirmationSheet];
  __weak ReadingListTableViewController* weakSelf = self;
  NSString* markAsReadTitle = l10n_util::GetNSStringWithFixup(
      IDS_IOS_READING_LIST_MARK_ALL_READ_ACTION);
  [self.markConfirmationSheet
      addItemWithTitle:markAsReadTitle
                action:^{
                  [weakSelf markItemsInSection:kSectionIdentifierUnread
                                withReadStatus:YES];
                  [weakSelf dismissMarkConfirmationSheet];
                }
                 style:UIAlertActionStyleDefault];
  NSString* markAsUnreadTitle = l10n_util::GetNSStringWithFixup(
      IDS_IOS_READING_LIST_MARK_ALL_UNREAD_ACTION);
  [self.markConfirmationSheet
      addItemWithTitle:markAsUnreadTitle
                action:^{
                  [weakSelf markItemsInSection:kSectionIdentifierRead
                                withReadStatus:NO];
                  [weakSelf dismissMarkConfirmationSheet];
                }
                 style:UIAlertActionStyleDefault];
  [self.markConfirmationSheet start];
}

- (void)performBatchTableViewUpdates:(void (^)(void))updates
                          completion:(void (^)(BOOL finished))completion {
  self.numberOfBatchOperationInProgress += 1;
  void (^releaseDataSource)(BOOL) = ^(BOOL finished) {
    // Set numberOfBatchOperationInProgress before calling completion, as
    // completion may trigger another change.
    DCHECK_GT(self.numberOfBatchOperationInProgress, 0);
    self.numberOfBatchOperationInProgress -= 1;
    if (completion) {
      completion(finished);
    }
  };
  [super performBatchTableViewUpdates:updates completion:releaseDataSource];
}

#pragma mark - ReadingListToolbarButtonCommands Helpers

// Creates a confirmation action sheet for the "Mark" toolbar button item.
- (void)initializeMarkConfirmationSheet {
  self.markConfirmationSheet = [self.toolbarManager
      markButtonConfirmationWithBaseViewController:self
                                           browser:_browser];

  __weak ReadingListTableViewController* weakSelf = self;
  [self.markConfirmationSheet
      addItemWithTitle:l10n_util::GetNSStringWithFixup(IDS_CANCEL)
                action:^{
                  [weakSelf dismissMarkConfirmationSheet];
                }
                 style:UIAlertActionStyleCancel];
}

#pragma mark - Sign-in Promo

- (void)promoStateChanged:(BOOL)promoEnabled
        promoConfigurator:(SigninPromoViewConfigurator*)promoConfigurator
            promoDelegate:(id<SigninPromoViewDelegate>)promoDelegate
                promoText:(NSString*)promoText {
  if (promoEnabled) {
    CHECK(![self.tableViewModel
        hasSectionForSectionIdentifier:kSectionIdentifierSignInPromo]);
    [self.tableViewModel
        insertSectionWithIdentifier:kSectionIdentifierSignInPromo
                            atIndex:0];
    TableViewSigninPromoItem* signInPromoItem =
        [[TableViewSigninPromoItem alloc] initWithType:kItemTypeSignInPromo];
    signInPromoItem.configurator = promoConfigurator;
    signInPromoItem.text = promoText;
    signInPromoItem.delegate = promoDelegate;
    [self.tableViewModel addItem:signInPromoItem
         toSectionWithIdentifier:kSectionIdentifierSignInPromo];
  } else {
    CHECK([self.tableViewModel
        hasSectionForSectionIdentifier:kSectionIdentifierSignInPromo]);
    [self.tableViewModel
        removeSectionWithIdentifier:kSectionIdentifierSignInPromo];
  }
  [self.tableView reloadData];
  [self updateEmptyViewTopMargin];
}

- (void)configureSigninPromoWithConfigurator:
            (SigninPromoViewConfigurator*)promoConfigurator
                             identityChanged:(BOOL)identityChanged {
  if (![self.tableViewModel
          hasSectionForSectionIdentifier:kSectionIdentifierSignInPromo]) {
    return;
  }

  NSIndexPath* indexPath =
      [self.tableViewModel indexPathForItemType:kItemTypeSignInPromo
                              sectionIdentifier:kSectionIdentifierSignInPromo];
  TableViewSigninPromoItem* signInPromoItem =
      base::apple::ObjCCast<TableViewSigninPromoItem>(
          [self.tableViewModel itemAtIndexPath:indexPath]);
  if (!signInPromoItem) {
    return;
  }

  signInPromoItem.configurator = promoConfigurator;
  [self reloadCellsForItems:@[ signInPromoItem ]
           withRowAnimation:UITableViewRowAnimationNone];

  // The sign-in promo view height may have been changed after the configurator
  // change, we need to update the empty view top margin according to it.
  [self updateEmptyViewTopMargin];
}

#pragma mark - Item Loading Helpers

// Uses self.dataSource to load the TableViewItems into self.tableViewModel.
- (void)loadItems {
  NSMutableArray<id<ReadingListListItem>>* readArray = [NSMutableArray array];
  NSMutableArray<id<ReadingListListItem>>* unreadArray = [NSMutableArray array];
  [self.dataSource fillReadItems:readArray unreadItems:unreadArray];
  [self loadItemsFromArray:unreadArray toSection:kSectionIdentifierUnread];
  [self loadItemsFromArray:readArray toSection:kSectionIdentifierRead];

  [self updateToolbarItems];
}

// Adds `items` to self.tableViewModel for the section designated by
// `sectionID`.
- (void)loadItemsFromArray:(NSArray<id<ReadingListListItem>>*)items
                 toSection:(ReadingListSectionIdentifier)sectionID {
  if (!items.count)
    return;

  TableViewModel* model = self.tableViewModel;
  [model addSectionWithIdentifier:sectionID];
  [model setHeader:[self headerForSectionIndex:sectionID]
      forSectionWithIdentifier:sectionID];
  __weak __typeof(self) weakSelf = self;
  for (TableViewItem<ReadingListListItem>* item in items) {
    item.type = kItemTypeItem;
    [model addItem:item toSectionWithIdentifier:sectionID];

    // This function is currently reloading the model.
    // It has been observed that the item just added is not fully available,
    // the model containing the item but the item count of the section not
    // being updated correctly.
    // Updating the favicon can lead to synchronous update of the item if the
    // icon is already available. To avoid causing a crash, update the trigger
    // the favicon asynchronously.
    // TODO(crbug.com/40240200): check the fix actually prevents crashing.
    __weak __typeof(item) weakItem = item;
    dispatch_async(dispatch_get_main_queue(), ^{
      if (weakSelf && weakItem) {
        [weakSelf.dataSource fetchFaviconForItem:weakItem];
      }
    });
  }
}

// Returns a TableViewTextItem that displays the title for the section
// designated by `sectionID`.
- (TableViewHeaderFooterItem*)headerForSectionIndex:
    (ReadingListSectionIdentifier)sectionID {
  TableViewTextHeaderFooterItem* header =
      [[TableViewTextHeaderFooterItem alloc] initWithType:kItemTypeHeader];

  switch (sectionID) {
    case kSectionIdentifierRead:
      header.text = l10n_util::GetNSString(IDS_IOS_READING_LIST_READ_HEADER);
      break;
    case kSectionIdentifierUnread:
      header.text = l10n_util::GetNSString(IDS_IOS_READING_LIST_UNREAD_HEADER);
      break;
    case kSectionIdentifierSignInPromo:
      header = nil;
      break;
  }
  return header;
}

#pragma mark - Toolbar Helpers

// Updates buttons displayed in the bottom toolbar.
- (void)updateToolbarItems {
  self.toolbarManager.editing = self.editingWithToolbarButtons;
  self.toolbarManager.hasReadItems =
      self.dataSource.hasElements && self.dataSource.hasReadElements;
  self.toolbarManager.selectionState = GetSelectionStateForSelectedCounts(
      self.selectedUnreadItemCount, self.selectedReadItemCount);
  if (self.toolbarManager.buttonItemsUpdated)
    [self setToolbarItems:[self.toolbarManager buttonItems] animated:YES];
  [self.toolbarManager updateMarkButtonTitle];
}

#pragma mark - Item Editing Helpers

// Returns `item` cast as a TableViewItem.
- (TableViewItem<ReadingListListItem>*)tableItemForReadingListItem:
    (id<ReadingListListItem>)item {
  return base::apple::ObjCCastStrict<TableViewItem<ReadingListListItem>>(item);
}

// Applies `updater` to the items in `section`. The updates are done in reverse
// order of the cells in the section to keep the order. Monitoring of the
// data source updates are suspended during this time.
- (void)updateItemsInSection:(ReadingListSectionIdentifier)section
             withItemUpdater:(ReadingListListItemUpdater)updater {
  DCHECK(updater);
  [self.dataSource beginBatchUpdates];
  NSArray* items = [self.tableViewModel itemsInSectionWithIdentifier:section];
  // Read the objects in reverse order to keep the order (last modified first).
  for (TableViewItem* item in [items reverseObjectEnumerator]) {
    // TODO(crbug.com/40263259): the runtime check will be replaced using new
    // methods implementations in TableViewItem and ReadingListTableViewItem.
    if ([item conformsToProtocol:@protocol(ReadingListListItem)]) {
      updater((id<ReadingListListItem>)item);
    }
  }
  [self.dataSource endBatchUpdates];
}

// Applies `updater` to the items in `indexPaths`. The updates are done in
// reverse order `indexPaths` to keep the order. The monitoring of the data
// source updates are suspended during this time.
- (void)updateItemsAtIndexPaths:(NSArray<NSIndexPath*>*)indexPaths
                withItemUpdater:(ReadingListListItemUpdater)updater {
  DCHECK(updater);
  [self.dataSource beginBatchUpdates];
  // Read the objects in reverse order to keep the order (last modified first).
  for (NSIndexPath* indexPath in [indexPaths reverseObjectEnumerator]) {
    TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
    // TODO(crbug.com/40263259): the runtime check will be replaced by new
    // methods implementations in TableViewItem and ReadingListTableViewItem.
    if ([item conformsToProtocol:@protocol(ReadingListListItem)]) {
      updater((id<ReadingListListItem>)item);
    }
  }
  [self.dataSource endBatchUpdates];
}

// Moves all the items from `fromSection` to `toSection` and removes the empty
// section from the collection.
- (void)moveItemsFromSection:(ReadingListSectionIdentifier)fromSection
                   toSection:(ReadingListSectionIdentifier)toSection {
  if (![self.tableViewModel hasSectionForSectionIdentifier:fromSection]) {
    return;
  }
  NSInteger sourceSection =
      [self.tableViewModel sectionForSectionIdentifier:fromSection];
  NSInteger itemCount =
      [self.tableViewModel numberOfItemsInSection:sourceSection];

  NSMutableArray* sortedIndexPaths = [NSMutableArray array];
  for (NSInteger row = 0; row < itemCount; ++row) {
    NSIndexPath* itemPath =
        [NSIndexPath indexPathForRow:row inSection:sourceSection];
    [sortedIndexPaths addObject:itemPath];
  }

  [self moveItemsAtIndexPaths:sortedIndexPaths toSection:toSection];
}

// Moves the items at `sortedIndexPaths` to `toSection`, removing any empty
// sections.
- (void)moveItemsAtIndexPaths:(NSArray*)sortedIndexPaths
                    toSection:(ReadingListSectionIdentifier)toSection {
  // Reconfigure cells, allowing the custom actions to be updated.
  for (NSIndexPath* indexPath in sortedIndexPaths) {
    if (![self.tableView cellForRowAtIndexPath:indexPath])
      continue;

    [[self.tableViewModel itemAtIndexPath:indexPath]
        configureCell:[self.tableView cellForRowAtIndexPath:indexPath]
           withStyler:self.styler];
  }

  NSInteger sectionCreatedIndex = [self initializeTableViewSection:toSection];
  void (^updates)(void) = ^{
    NSInteger sectionIndex =
        [self.tableViewModel sectionForSectionIdentifier:toSection];

    NSInteger newItemIndex = 0;
    for (NSIndexPath* indexPath in sortedIndexPaths) {
      // The `sortedIndexPaths` is a copy of the index paths before the
      // destination section has been added if necessary. The section part of
      // the index potentially needs to be updated.
      NSInteger updatedSection = indexPath.section;
      if (updatedSection >= sectionCreatedIndex)
        updatedSection++;
      if (updatedSection == sectionIndex) {
        // The item is already in the targeted section, there is no need to move
        // it.
        continue;
      }

      NSIndexPath* updatedIndexPath =
          [NSIndexPath indexPathForItem:indexPath.row inSection:updatedSection];
      NSIndexPath* indexPathForModel =
          [NSIndexPath indexPathForItem:indexPath.item - newItemIndex
                              inSection:updatedSection];

      // Index of the item in the new section. The newItemIndex is the index of
      // this item in the targeted section.
      NSIndexPath* newIndexPath =
          [NSIndexPath indexPathForItem:newItemIndex++ inSection:sectionIndex];

      [self moveItemWithModelIndex:indexPathForModel
                    tableViewIndex:updatedIndexPath
                           toIndex:newIndexPath];
    }
  };
  void (^completion)(BOOL) = ^(BOOL) {
    [self batchEditDidFinish];
  };
  [self performBatchTableViewUpdates:updates completion:completion];
}

// Moves the ListItem within self.tableViewModel at `modelIndex` and the
// UITableViewCell at `tableViewIndex` to `toIndexPath`.
- (void)moveItemWithModelIndex:(NSIndexPath*)modelIndex
                tableViewIndex:(NSIndexPath*)tableViewIndex
                       toIndex:(NSIndexPath*)toIndexPath {
  TableViewModel* model = self.tableViewModel;
  TableViewItem* item = [model itemAtIndexPath:modelIndex];

  // Move the item in `model`.
  [self deleteItemAtIndexPathFromModel:modelIndex];
  NSInteger toSectionID =
      [model sectionIdentifierForSectionIndex:toIndexPath.section];
  [model insertItem:item
      inSectionWithIdentifier:toSectionID
                      atIndex:toIndexPath.row];

  // Move the cells in the table view.
  [self.tableView moveRowAtIndexPath:tableViewIndex toIndexPath:toIndexPath];
}

// Makes sure the table view section with `sectionID` exists with the correct
// header. Returns the index of the new section in the table view, or
// NSIntegerMax if no section has been created.
- (NSInteger)initializeTableViewSection:
    (ReadingListSectionIdentifier)sectionID {
  TableViewModel* model = self.tableViewModel;
  if ([model hasSectionForSectionIdentifier:sectionID])
    return NSIntegerMax;

  NSInteger sectionIndex = [self newSectionIndexForId:sectionID];
  void (^updates)(void) = ^{
    [model insertSectionWithIdentifier:sectionID atIndex:sectionIndex];
    [model setHeader:[self headerForSectionIndex:sectionID]
        forSectionWithIdentifier:sectionID];
    [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                  withRowAnimation:UITableViewRowAnimationMiddle];
  };
  [self performBatchTableViewUpdates:updates completion:nil];

  return sectionIndex;
}

// Whether the model has items in `sectionID`.
- (BOOL)hasItemInSection:(ReadingListSectionIdentifier)sectionID {
  return [self itemsForSection:sectionID].count > 0;
}

// Deletes the items at `indexPaths`, exiting editing and removing empty
// sections upon completion.
- (void)deleteItemsAtIndexPaths:(NSArray<NSIndexPath*>*)indexPaths {
  [self deleteItemsAtIndexPaths:indexPaths
                     endEditing:YES
            removeEmptySections:YES];
}

// Deletes the items at `indexPaths`.  Exits editing mode if `endEditing` is
// YES.  Removes empty sections upon completion if `removeEmptySections` is YES.
- (void)deleteItemsAtIndexPaths:(NSArray<NSIndexPath*>*)indexPaths
                     endEditing:(BOOL)endEditing
            removeEmptySections:(BOOL)removeEmptySections {
  // Delete the items in the data source and exit editing mode.
  ReadingListListItemUpdater updater = ^(id<ReadingListListItem> item) {
    [self.dataSource removeEntryFromItem:item];
  };
  [self updateItemsAtIndexPaths:indexPaths withItemUpdater:updater];
  // Update the model and table view for the deleted items.
  UITableView* tableView = self.tableView;
  NSArray* sortedIndexPaths =
      [indexPaths sortedArrayUsingSelector:@selector(compare:)];
  void (^updates)(void) = ^{
    // Enumerate in reverse order to delete the items from the model.
    for (NSIndexPath* indexPath in [sortedIndexPaths reverseObjectEnumerator]) {
      [self deleteItemAtIndexPathFromModel:indexPath];
    }
    [tableView deleteRowsAtIndexPaths:indexPaths
                     withRowAnimation:UITableViewRowAnimationAutomatic];
  };

  void (^completion)(BOOL) = nil;
  if (removeEmptySections) {
    completion = ^(BOOL) {
      [self batchEditDidFinish];
    };
  }
  [self performBatchTableViewUpdates:updates completion:completion];
  if (endEditing) {
    [self exitEditingModeAnimated:YES];
  }
}

// Deletes the ListItem corresponding to `indexPath` in the model.
- (void)deleteItemAtIndexPathFromModel:(NSIndexPath*)indexPath {
  TableViewModel* model = self.tableViewModel;
  NSInteger sectionID =
      [model sectionIdentifierForSectionIndex:indexPath.section];
  NSInteger itemType = [model itemTypeForIndexPath:indexPath];
  NSUInteger index = [model indexInItemTypeForIndexPath:indexPath];
  [model removeItemWithType:itemType
      fromSectionWithIdentifier:sectionID
                        atIndex:index];
}

// Marks all the items at `indexPaths` as read or unread depending on `read`.
- (void)markItemsAtIndexPaths:(NSArray<NSIndexPath*>*)indexPaths
               withReadStatus:(BOOL)read {
  // Record metric.
  base::RecordAction(base::UserMetricsAction(
      read ? "MobileReadingListMarkRead" : "MobileReadingListMarkUnread"));

  // Mark the items as `read` and exit editing.
  ReadingListListItemUpdater updater = ^(id<ReadingListListItem> item) {
    [self.dataSource setReadStatus:read forItem:item];
  };
  NSArray* sortedIndexPaths =
      [indexPaths sortedArrayUsingSelector:@selector(compare:)];
  [self updateItemsAtIndexPaths:sortedIndexPaths withItemUpdater:updater];

  // Move the items to the appropriate section.
  ReadingListSectionIdentifier toSection =
      read ? kSectionIdentifierRead : kSectionIdentifierUnread;
  [self moveItemsAtIndexPaths:sortedIndexPaths toSection:toSection];
  [self exitEditingModeAnimated:YES];
}

// Marks items from `section` with as read or unread dending on `read`.
- (void)markItemsInSection:(ReadingListSectionIdentifier)section
            withReadStatus:(BOOL)read {
  if (![self.tableViewModel hasSectionForSectionIdentifier:section]) {
    [self exitEditingModeAnimated:YES];
    return;
  }

  // Mark the items as `read` and exit editing.
  ReadingListListItemUpdater updater = ^(id<ReadingListListItem> item) {
    [self.dataSource setReadStatus:read forItem:item];
  };
  [self updateItemsInSection:section withItemUpdater:updater];

  // Move the items to the appropriate section.
  ReadingListSectionIdentifier toSection =
      read ? kSectionIdentifierRead : kSectionIdentifierUnread;
  [self moveItemsFromSection:section toSection:toSection];
  [self exitEditingModeAnimated:YES];
}

// Cleanup function called in the completion block of editing operations.
- (void)batchEditDidFinish {
  // Reload the items if the datasource was modified during the edit.
  [self reloadDataIfNeededAndNotEditing];

  // Remove any newly emptied sections.
  [self removeEmptySections];
}

// Removes the empty sections from the table and the model.  Returns the number
// of removed sections.
- (NSUInteger)removeEmptySections {
  UITableView* tableView = self.tableView;
  TableViewModel* model = self.tableViewModel;
  __block NSUInteger removedSectionCount = 0;
  void (^updates)(void) = ^{
    ReadingListSectionIdentifier sections[] = {kSectionIdentifierRead,
                                               kSectionIdentifierUnread};
    for (size_t i = 0; i < std::size(sections); ++i) {
      ReadingListSectionIdentifier section = sections[i];

      if ([model hasSectionForSectionIdentifier:section] &&
          ![self hasItemInSection:section]) {
        // If `section` has no items, remove it from the model and the table
        // view.
        NSInteger sectionIndex = [model sectionForSectionIdentifier:section];
        [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                 withRowAnimation:UITableViewRowAnimationFade];
        [model removeSectionWithIdentifier:section];
        ++removedSectionCount;
      }
    }
  };

  [self performBatchTableViewUpdates:updates completion:nil];
  if (!self.dataSource.hasElements)
    [self tableIsEmpty];
  else
    [self updateToolbarItems];
  return removedSectionCount;
}

// Resets self.editing to NO, optionally with animation.
- (void)exitEditingModeAnimated:(BOOL)animated {
  self.markConfirmationSheet = nil;
  [self setEditing:NO animated:animated];
}

#pragma mark - Accessibility

- (BOOL)accessibilityPerformEscape {
  base::RecordAction(
      base::UserMetricsAction("MobileReadingListAccessibilityClose"));
  [self.delegate dismissReadingListListViewController:self];
  return YES;
}

#pragma mark - Private

// Called when the table is not empty.
- (void)tableIsNotEmpty {
  [self loadItems];
  [self.audience readingListHasItems:YES];
  self.tableView.alwaysBounceVertical = YES;
  [self removeEmptyTableView];
}

// Called when the table is empty.
- (void)tableIsEmpty {
  // It is necessary to reloadData now, before modifying the view which will
  // force a layout.
  // If the window is not displayed (e.g. in an inactive scene) the number of
  // elements may be outdated and the layout triggered by this function will
  // generate access non-existing items.
  [self.tableView reloadData];
  UIImage* emptyImage = [UIImage imageNamed:@"reading_list_empty"];
  NSString* title =
      l10n_util::GetNSString(IDS_IOS_READING_LIST_NO_ENTRIES_TITLE);
  NSString* subtitle =
      l10n_util::GetNSString(IDS_IOS_READING_LIST_NO_ENTRIES_MESSAGE);
  [self addEmptyTableViewWithImage:emptyImage title:title subtitle:subtitle];
  self.navigationItem.largeTitleDisplayMode =
      UINavigationItemLargeTitleDisplayModeNever;
  self.tableView.alwaysBounceVertical = NO;
  [self.audience readingListHasItems:NO];
  [self updateEmptyViewTopMargin];
}

// Reloads the data if source change during the edit mode and if it is now safe
// to do so (local edits are done).
- (void)reloadDataIfNeededAndNotEditing {
  if (self.dataSourceModifiedWhileEditing &&
      self.numberOfBatchOperationInProgress == 0 && !self.editing) {
    [self reloadData];
  }
}

// The empty view has different top margin according to the sign-in promo view
// presence. This method needs to be called after the promo view changes.
- (void)updateEmptyViewTopMargin {
  BOOL promoViewVisible =
      [self.tableViewModel hasItemForItemType:kItemTypeSignInPromo
                            sectionIdentifier:kSectionIdentifierSignInPromo];
  if (promoViewVisible && !self.dataSource.hasElements) {
    NSIndexPath* promoIndexPath = [self.tableViewModel
        indexPathForItemType:kItemTypeSignInPromo
           sectionIdentifier:kSectionIdentifierSignInPromo];
    UITableViewCell* promoCell =
        [self.tableView cellForRowAtIndexPath:promoIndexPath];
    CGFloat promoHeight = promoCell.bounds.size.height;
    [self setEmptyViewTopOffset:promoHeight];
  } else {
    [self setEmptyViewTopOffset:0.0];
  }
}

// Computes the index of the section to be created, given the sections that
// already exist.
- (NSInteger)newSectionIndexForId:(ReadingListSectionIdentifier)newSectionID {
  ReadingListSectionIdentifier sections[] = {kSectionIdentifierSignInPromo,
                                             kSectionIdentifierUnread,
                                             kSectionIdentifierRead};
  NSInteger sectionIndex = 0;
  for (ReadingListSectionIdentifier section : sections) {
    if (newSectionID == section) {
      return sectionIndex;
    }
    if ([self hasItemInSection:section]) {
      sectionIndex++;
    }
  }
  NOTREACHED_IN_MIGRATION();
  return 0;
}

- (void)dismissMarkConfirmationSheet {
  [_markConfirmationSheet stop];
  _markConfirmationSheet = nil;
}
@end