chromium/ios/chrome/browser/ui/popup_menu/overflow_menu/overflow_menu_orderer.mm

// Copyright 2023 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/popup_menu/overflow_menu/overflow_menu_orderer.h"

#import <unordered_set>

#import "base/containers/contains.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "components/prefs/pref_service.h"
#import "components/prefs/scoped_user_pref_update.h"
#import "ios/chrome/browser/commerce/model/push_notification/push_notification_feature.h"
#import "ios/chrome/browser/shared/model/prefs/pref_backed_boolean.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/destination_usage_history/constants.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/destination_usage_history/destination_usage_history.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/feature_flags.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/overflow_menu_action_provider.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/overflow_menu_metrics.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/overflow_menu_swift.h"
#import "ios/chrome/browser/ui/whats_new/whats_new_util.h"

namespace {
// The dictionary key used for storing rankings.
const char kRankingKey[] = "ranking";

// The dictionary key used for storing the shown action ordering.
const char kShownActionsKey[] = "shown";

// The dictionary key used for storing the hidden action ordering.
const char kHiddenActionsKey[] = "hidden";

// The dictionary key used for storing the impressions remaining.
const char kImpressionsRemainingKey[] = "impressions_remaining";

// The dictionary key used for storing the badge type.
const char kBadgeTypeKey[] = "badge_type";

// The dictionary key used for storing whether the badge is feature driven.
const char kIsFeatureDrivenBadgeKey[] = "is_feature_driven_badge";

// Ingests base::Value::List of destination names (strings) (`from` list),
// converts each string to an overflow_menu::Destination, then appends each
// destination to a vector (`to` vector). Skips over invalid or malformed list
// items.
void AppendDestinationsToVector(const base::Value::List& from,
                                std::vector<overflow_menu::Destination>& to) {
  for (const auto& value : from) {
    if (!value.is_string()) {
      continue;
    }

    to.push_back(overflow_menu::DestinationForStringName(value.GetString()));
  }
}

// Ingests base::Value::List of destination names (strings) (`from` list),
// converts each string to an overflow_menu::Destination, then adds each
// destination to a set (`to` set). Skips over invalid or malformed list
// items.
void AddDestinationsToSet(const base::Value::List& from,
                          std::set<overflow_menu::Destination>& to) {
  for (const auto& value : from) {
    if (!value.is_string()) {
      continue;
    }

    to.insert(overflow_menu::DestinationForStringName(value.GetString()));
  }
}

// Inserts `destination` in `output` at kNewDestinationsInsertionIndex and
// clear `destination` from the `destinationsToAdd` set.
void InsertDestination(overflow_menu::Destination destination,
                       std::set<overflow_menu::Destination>& destinationsToAdd,
                       DestinationRanking& output,
                       bool insertAtEnd) {
  if (insertAtEnd) {
    output.push_back(destination);
  } else {
    const int insertionIndex = std::min(
        output.size() - 1, static_cast<size_t>(kNewDestinationsInsertionIndex));

    output.insert(output.begin() + insertionIndex, destination);
  }

  destinationsToAdd.erase(destination);
}

// Simple data struct to bundle the two lists of destinations together.
struct DestinationOrderData {
  DestinationRanking shownDestinations;
  DestinationRanking hiddenDestinations;

  bool empty() const {
    return shownDestinations.empty() && hiddenDestinations.empty();
  }
};

// Simple data struct to bundle the two lists of actions together.
struct ActionOrderData {
  ActionRanking shownActions;
  ActionRanking hiddenActions;

  bool empty() const { return shownActions.empty() && hiddenActions.empty(); }
};

struct BadgeData {
  int impressionsRemaining;
  BadgeType badgeType;
  bool isFeatureDrivenBadge;
};

// Creates a `BadgeData` from the provided dict.
std::optional<BadgeData> BadgeDataFromDict(const base::Value::Dict& dict) {
  BadgeData badgeData;

  std::optional<int> impressionsRemaining =
      dict.FindInt(kImpressionsRemainingKey);
  if (!impressionsRemaining) {
    return std::nullopt;
  }
  badgeData.impressionsRemaining = impressionsRemaining.value();

  std::optional<bool> isFeatureDrivenBadge =
      dict.FindBool(kIsFeatureDrivenBadgeKey);
  if (!isFeatureDrivenBadge) {
    return std::nullopt;
  }
  badgeData.isFeatureDrivenBadge = isFeatureDrivenBadge.value();

  const std::string* badgeType = dict.FindString(kBadgeTypeKey);
  if (!badgeType) {
    return std::nullopt;
  }
  badgeData.badgeType = [OverflowMenuDestination
      badgeTypeFromString:base::SysUTF8ToNSString(*badgeType)];

  return badgeData;
}

// Creates a `base::Value::Dict` from the provided `BadgeData`.
base::Value::Dict DictFromBadgeData(const BadgeData badgeData) {
  std::string badgeTypeString = base::SysNSStringToUTF8(
      [OverflowMenuDestination stringFromBadgeType:badgeData.badgeType]);
  return base::Value::Dict()
      .Set(kImpressionsRemainingKey, badgeData.impressionsRemaining)
      .Set(kIsFeatureDrivenBadgeKey, badgeData.isFeatureDrivenBadge)
      .Set(kBadgeTypeKey, badgeTypeString);
}

}  // namespace

@interface OverflowMenuOrderer ()

// The destination usage history, which (1) tracks which items from the carousel
// are clicked, and (2) suggests a sorted order for carousel menu items.
@property(nonatomic, strong) DestinationUsageHistory* destinationUsageHistory;

@end

@implementation OverflowMenuOrderer {
  // Whether the current menu is for an incognito page.
  BOOL _isIncognito;

  // New destinations recently added to the overflow menu carousel that have not
  // yet been clicked by the user.
  std::set<overflow_menu::Destination> _untappedDestinations;

  // The data for the current destinations ordering and show/hide state.
  DestinationOrderData _destinationOrderData;

  // The data for the current actions ordering and show/hide state.
  ActionOrderData _actionOrderData;

  PrefBackedBoolean* _destinationUsageHistoryEnabled;

  // The data for which destinations currently have badges and how many
  // impressions they have remaining.
  std::map<overflow_menu::Destination, BadgeData> _destinationBadgeData;
}

@synthesize actionCustomizationModel = _actionCustomizationModel;
@synthesize destinationCustomizationModel = _destinationCustomizationModel;

- (instancetype)initWithIsIncognito:(BOOL)isIncognito {
  if ((self = [super init])) {
    _isIncognito = isIncognito;
  }
  return self;
}

- (void)disconnect {
  [self.destinationUsageHistory stop];
  self.destinationUsageHistory = nil;
}

#pragma mark - Property Setters/Getters

- (void)setLocalStatePrefs:(PrefService*)localStatePrefs {
  _localStatePrefs = localStatePrefs;

  if (!_isIncognito) {
    self.destinationUsageHistory =
        [[DestinationUsageHistory alloc] initWithPrefService:localStatePrefs];
    self.destinationUsageHistory.visibleDestinationsCount =
        self.visibleDestinationsCount;
    [self.destinationUsageHistory start];
  }

  _destinationUsageHistoryEnabled = [[PrefBackedBoolean alloc]
      initWithPrefService:_localStatePrefs
                 prefName:prefs::kOverflowMenuDestinationUsageHistoryEnabled];

  [self loadDestinationsFromPrefs];
  [self loadActionsFromPrefs];
}

- (void)setVisibleDestinationsCount:(int)visibleDestinationsCount {
  _visibleDestinationsCount = visibleDestinationsCount;
  self.destinationUsageHistory.visibleDestinationsCount =
      self.visibleDestinationsCount;
}

// Lazily create action customization model.
- (ActionCustomizationModel*)actionCustomizationModel {
  if (_actionCustomizationModel) {
    return _actionCustomizationModel;
  }

  [self updateActionOrderData];

  NSMutableArray<OverflowMenuAction*>* actions = [[NSMutableArray alloc] init];
  for (overflow_menu::ActionType action : _actionOrderData.shownActions) {
    if (OverflowMenuAction* overflowMenuAction =
            [self.actionProvider customizationActionForActionType:action]) {
      [actions addObject:overflowMenuAction];
    }
  }
  for (overflow_menu::ActionType action : _actionOrderData.hiddenActions) {
    if (OverflowMenuAction* overflowMenuAction =
            [self.actionProvider customizationActionForActionType:action]) {
      overflowMenuAction.shown = NO;
      [actions addObject:overflowMenuAction];
    }
  }

  _actionCustomizationModel =
      [[ActionCustomizationModel alloc] initWithActions:actions];
  return _actionCustomizationModel;
}

// Lazily create destination customization model.
- (DestinationCustomizationModel*)destinationCustomizationModel {
  if (_destinationCustomizationModel) {
    return _destinationCustomizationModel;
  }

  [self initializeDestinationOrderDataIfEmpty];

  NSMutableArray<OverflowMenuDestination*>* destinations =
      [[NSMutableArray alloc] init];
  for (overflow_menu::Destination destination :
       _destinationOrderData.shownDestinations) {
    if (OverflowMenuDestination* overflowMenuDestination =
            [self.destinationProvider
                customizationDestinationForDestinationType:destination]) {
      [destinations addObject:overflowMenuDestination];
    }
  }
  for (overflow_menu::Destination destination :
       _destinationOrderData.hiddenDestinations) {
    if (OverflowMenuDestination* overflowMenuDestination =
            [self.destinationProvider
                customizationDestinationForDestinationType:destination]) {
      overflowMenuDestination.shown = NO;
      [destinations addObject:overflowMenuDestination];
    }
  }

  _destinationCustomizationModel = [[DestinationCustomizationModel alloc]
         initWithDestinations:destinations
      destinationUsageEnabled:_destinationUsageHistoryEnabled.value];
  return _destinationCustomizationModel;
}

- (BOOL)isDestinationCustomizationInProgress {
  return _destinationCustomizationModel != nil;
}

#pragma mark - Public

- (void)recordClickForDestination:(overflow_menu::Destination)destination {
  _untappedDestinations.erase(destination);

  if (base::Contains(_destinationBadgeData, destination) &&
      !_destinationBadgeData[destination].isFeatureDrivenBadge) {
    _destinationBadgeData.erase(destination);
  }

  [self flushDestinationsToPrefs];

  [self.destinationUsageHistory recordClickForDestination:destination];
}

- (void)reorderDestinationsForInitialMenu {
  [self initializeDestinationOrderDataIfEmpty];

  DestinationRanking availableDestinations =
      [self.destinationProvider baseDestinations];

  if (IsOverflowMenuCustomizationEnabled()) {
    DestinationRanking badgedRanking =
        [self customizationRankingAfterBadgingWithAvailableDestinations:
                  availableDestinations];
    _destinationOrderData.shownDestinations = badgedRanking;
    [self flushDestinationsToPrefs];
  }

  // If customization is enabled, then skip destination usage history if there
  // are current badges, as those have more important positions.
  BOOL hasBadgeWithImpressions = NO;
  for (const auto& [key, value] : _destinationBadgeData) {
    if (value.impressionsRemaining > 0) {
      hasBadgeWithImpressions = YES;
      break;
    }
  }
  BOOL skipDestinationUsageHistory =
      IsOverflowMenuCustomizationEnabled() &&
      (hasBadgeWithImpressions || !_destinationUsageHistoryEnabled.value);

  if (!skipDestinationUsageHistory && self.destinationUsageHistory) {
    _destinationOrderData.shownDestinations = [self.destinationUsageHistory
        sortedDestinationsFromCurrentRanking:_destinationOrderData
                                                 .shownDestinations
                       availableDestinations:availableDestinations];

    [self flushDestinationsToPrefs];
  }

  if (!IsOverflowMenuCustomizationEnabled()) {
    DestinationRanking badgedRanking = [self
        rankingAfterBadgingWithAvailableDestinations:availableDestinations];

    _destinationOrderData.shownDestinations = badgedRanking;
    [self flushDestinationsToPrefs];
  }

  self.model.destinations = [self destinationsFromCurrentRanking];
}

- (void)updateDestinations {
  [self initializeDestinationOrderDataIfEmpty];
  self.model.destinations = [self destinationsFromCurrentRanking];
}

- (void)updatePageActions {
  [self.pageActionsGroup setActionsWithAnimation:[self pageActions]];
}

- (void)updateForMenuDisappearance {
  // With Overflow Menu Customization, badge impressions need to be tracked.
  if (IsOverflowMenuCustomizationEnabled()) {
    // If spotlight debugging is enabled, an extra destination is auto-inserted
    // at the beginning.
    NSUInteger badgeImpressionLastIndex =
        (experimental_flags::IsSpotlightDebuggingEnabled())
            ? kNewDestinationsInsertionIndex + 1
            : kNewDestinationsInsertionIndex;

    NSRange impressedRange = NSMakeRange(
        0, MIN(badgeImpressionLastIndex + 1, self.model.destinations.count));
    for (OverflowMenuDestination* menuDestination :
         [self.model.destinations subarrayWithRange:impressedRange]) {
      overflow_menu::Destination destination =
          static_cast<overflow_menu::Destination>(menuDestination.destination);
      auto it = _destinationBadgeData.find(destination);
      if (it == _destinationBadgeData.end()) {
        continue;
      }
      // If the badge is feature-driven, just decrease its impression count
      // until it hits 0. Otherwise, remove it when it hits 0.
      if (it->second.isFeatureDrivenBadge) {
        it->second.impressionsRemaining =
            std::max(0, it->second.impressionsRemaining - 1);
      } else {
        it->second.impressionsRemaining = it->second.impressionsRemaining - 1;
        if (it->second.impressionsRemaining <= 0) {
          _destinationBadgeData.erase(destination);
        }
      }
    }
  }
}

- (void)commitActionsUpdate {
  if (!_actionCustomizationModel.hasChanged) {
    [self cancelActionsUpdate];
    return;
  }

  ActionOrderData actionOrderData;
  for (OverflowMenuAction* action in self.actionCustomizationModel
           .shownActions) {
    actionOrderData.shownActions.push_back(
        static_cast<overflow_menu::ActionType>(action.actionType));
  }

  for (OverflowMenuAction* action in self.actionCustomizationModel
           .hiddenActions) {
    actionOrderData.hiddenActions.push_back(
        static_cast<overflow_menu::ActionType>(action.actionType));
  }

  [self recordMetricsForActionCustomizationWithNewOrderData:actionOrderData];

  _actionOrderData = actionOrderData;
  [self flushActionsToPrefs];

  [self updatePageActions];

  // Reset customization model so next customization can start fresh.
  _actionCustomizationModel = nil;
}

- (void)commitDestinationsUpdate {
  if (!_destinationCustomizationModel.hasChanged) {
    [self cancelDestinationsUpdate];
    return;
  }

  DestinationOrderData orderData;
  for (OverflowMenuDestination* destination in self
           .destinationCustomizationModel.shownDestinations) {
    orderData.shownDestinations.push_back(
        static_cast<overflow_menu::Destination>(destination.destination));
  }

  for (OverflowMenuDestination* destination in self
           .destinationCustomizationModel.hiddenDestinations) {
    orderData.hiddenDestinations.push_back(
        static_cast<overflow_menu::Destination>(destination.destination));
  }

  [self recordMetricsForDestinationCustomizationWithNewOrderData:orderData];

  [self eraseBadgesAfterDestinationCustomization];

  _destinationOrderData = orderData;
  // If Destination Usage History is being reenabled, add all hidden
  // destinations back to the shown list. Destination Usage History only acts on
  // all destinations at once.
  if (!_destinationUsageHistoryEnabled.value &&
      _destinationCustomizationModel.destinationUsageEnabled) {
    _destinationOrderData.shownDestinations.insert(
        _destinationOrderData.shownDestinations.end(),
        _destinationOrderData.hiddenDestinations.begin(),
        _destinationOrderData.hiddenDestinations.end());
    _destinationOrderData.hiddenDestinations.clear();
    [self.destinationUsageHistory clearStoredClickData];
  }
  _destinationUsageHistoryEnabled.value =
      _destinationCustomizationModel.destinationUsageEnabled;
  [self flushDestinationsToPrefs];

  [self.model
      setDestinationsWithAnimation:[self destinationsFromCurrentRanking]];

  // Reset customization model so next customization can start fresh.
  _destinationCustomizationModel = nil;
}

- (void)cancelActionsUpdate {
  _actionCustomizationModel = nil;
}

- (void)cancelDestinationsUpdate {
  _destinationCustomizationModel = nil;
}

- (void)customizationUpdateToggledShown:(BOOL)shown
                    forLinkedActionType:(overflow_menu::ActionType)actionType
                         actionSubtitle:(NSString*)actionSubtitle {
  if (!_actionCustomizationModel) {
    return;
  }

  OverflowMenuAction* correspondingAction;
  for (OverflowMenuAction* action in _actionCustomizationModel.actionsGroup
           .actions) {
    if (action.actionType == static_cast<int>(actionType)) {
      correspondingAction = action;
      break;
    }
  }

  if (!correspondingAction) {
    return;
  }

  if (shown) {
    correspondingAction.subtitle = nil;
  } else {
    correspondingAction.highlighted = YES;
    correspondingAction.subtitle = actionSubtitle;
  }
}

#pragma mark - Private

// Loads the stored destinations data from local prefs/disk.
- (void)loadDestinationsFromPrefs {
  // Fetch the stored list of newly-added, unclicked destinations, then update
  // `_untappedDestinations` with its data.
  AddDestinationsToSet(
      _localStatePrefs->GetList(prefs::kOverflowMenuNewDestinations),
      _untappedDestinations);

  if (IsOverflowMenuCustomizationEnabled()) {
    const base::Value::List& storedHiddenDestinations =
        _localStatePrefs->GetList(prefs::kOverflowMenuHiddenDestinations);
    AppendDestinationsToVector(storedHiddenDestinations,
                               _destinationOrderData.hiddenDestinations);

    const base::Value::Dict& storedBadgeData =
        _localStatePrefs->GetDict(prefs::kOverflowMenuDestinationBadgeData);

    for (const auto&& [key, value] : storedBadgeData) {
      if (!value.is_dict()) {
        continue;
      }

      std::optional<BadgeData> badgeData = BadgeDataFromDict(value.GetDict());
      if (!badgeData) {
        continue;
      }

      overflow_menu::Destination destination =
          overflow_menu::DestinationForStringName(key);
      _destinationBadgeData[destination] = badgeData.value();
    }
  }

  [self loadShownDestinationsPref];

  // If the customization flag was enabled in the past and users hid
  // destinations make sure to add those back to the shown list, if the flag
  // becomes disabled.
  if (!IsOverflowMenuCustomizationEnabled()) {
    const base::Value::List& storedHiddenDestinations =
        _localStatePrefs->GetList(prefs::kOverflowMenuHiddenDestinations);
    AppendDestinationsToVector(storedHiddenDestinations,
                               _destinationOrderData.shownDestinations);
    _localStatePrefs->ClearPref(prefs::kOverflowMenuHiddenDestinations);

    // If Destination Usage History needs to be reenabled, then clear any stored
    // data.
    if (!_destinationUsageHistoryEnabled.value) {
      _destinationUsageHistoryEnabled.value = YES;
      [self.destinationUsageHistory clearStoredClickData];
    }

    [self flushDestinationsToPrefs];
  }
}

// Loads and migrates the shown destinations pref from disk.
- (void)loadShownDestinationsPref {
  // First try to load new pref.
  const base::Value::List& storedRanking =
      _localStatePrefs->GetList(prefs::kOverflowMenuDestinationsOrder);
  if (storedRanking.size() > 0) {
    AppendDestinationsToVector(storedRanking,
                               _destinationOrderData.shownDestinations);
    return;
  }
  // Fall back to old key.
  ScopedDictPrefUpdate storedUsageHistoryUpdate(
      _localStatePrefs, prefs::kOverflowMenuDestinationUsageHistory);
  base::Value::List* oldRanking =
      storedUsageHistoryUpdate->FindList(kRankingKey);
  if (!oldRanking) {
    return;
  }
  base::Value::List& oldRankingRef = *oldRanking;
  _localStatePrefs->SetList(prefs::kOverflowMenuDestinationsOrder,
                            oldRankingRef.Clone());

  AppendDestinationsToVector(oldRankingRef,
                             _destinationOrderData.shownDestinations);
  storedUsageHistoryUpdate->Remove(kRankingKey);
}

// Load the stored actions data from local prefs/disk.
- (void)loadActionsFromPrefs {
  const base::Value::Dict& storedActions =
      _localStatePrefs->GetDict(prefs::kOverflowMenuActionsOrder);
  ActionOrderData actionOrderData;

  const base::Value::List* shownActions =
      storedActions.FindList(kShownActionsKey);
  if (shownActions) {
    for (const auto& value : *shownActions) {
      if (!value.is_string()) {
        continue;
      }

      actionOrderData.shownActions.push_back(
          overflow_menu::ActionTypeForStringName(value.GetString()));
    }
  }

  const base::Value::List* hiddenActions =
      storedActions.FindList(kHiddenActionsKey);
  if (hiddenActions) {
    for (const auto& value : *hiddenActions) {
      if (!value.is_string()) {
        continue;
      }

      actionOrderData.hiddenActions.push_back(
          overflow_menu::ActionTypeForStringName(value.GetString()));
    }
  }
  _actionOrderData = actionOrderData;
}

// Write stored destination data back to local prefs/disk.
- (void)flushDestinationsToPrefs {
  if (!_localStatePrefs) {
    return;
  }

  // Flush the new destinations ranking to Prefs.
  base::Value::List ranking;

  for (overflow_menu::Destination destination :
       _destinationOrderData.shownDestinations) {
    ranking.Append(overflow_menu::StringNameForDestination(destination));
  }

  _localStatePrefs->SetList(prefs::kOverflowMenuDestinationsOrder,
                            std::move(ranking));

  // Flush list of hidden destinations to Prefs.
  if (IsOverflowMenuCustomizationEnabled()) {
    base::Value::List hiddenDestinations;

    for (overflow_menu::Destination destination :
         _destinationOrderData.hiddenDestinations) {
      hiddenDestinations.Append(
          overflow_menu::StringNameForDestination(destination));
    }

    _localStatePrefs->SetList(prefs::kOverflowMenuHiddenDestinations,
                              std::move(hiddenDestinations));

    // Flush dict of badge data to Prefs.
    base::Value::Dict badgeDataPref;
    for (const auto& [destination, badgeData] : _destinationBadgeData) {
      std::string destinationKey =
          overflow_menu::StringNameForDestination(destination);
      badgeDataPref.Set(destinationKey, DictFromBadgeData(badgeData));
    }

    _localStatePrefs->SetDict(prefs::kOverflowMenuDestinationBadgeData,
                              std::move(badgeDataPref));
  }

  // Flush the new untapped destinations to Prefs.
  ScopedListPrefUpdate untappedDestinationsUpdate(
      _localStatePrefs, prefs::kOverflowMenuNewDestinations);

  untappedDestinationsUpdate->clear();

  for (overflow_menu::Destination untappedDestination : _untappedDestinations) {
    untappedDestinationsUpdate->Append(
        overflow_menu::StringNameForDestination(untappedDestination));
  }
}

// Write stored action data back to local prefs/disk.
- (void)flushActionsToPrefs {
  if (!_localStatePrefs) {
    return;
  }
  base::Value::Dict storedActions;

  base::Value::List shownActions;
  for (overflow_menu::ActionType action : _actionOrderData.shownActions) {
    shownActions.Append(overflow_menu::StringNameForActionType(action));
  }

  base::Value::List hiddenActions;
  for (overflow_menu::ActionType action : _actionOrderData.hiddenActions) {
    hiddenActions.Append(overflow_menu::StringNameForActionType(action));
  }

  storedActions.Set(kShownActionsKey, std::move(shownActions));
  storedActions.Set(kHiddenActionsKey, std::move(hiddenActions));

  _localStatePrefs->SetDict(prefs::kOverflowMenuActionsOrder,
                            std::move(storedActions));
}

// Uses the current `actionProvider` to add any new actions to the shown list.
// This handles new users with no stored data and new actions added.
- (void)updateActionOrderData {
  ActionRanking availableActions = [self.actionProvider basePageActions];

  // Add any available actions not present in shown or hidden to the shown list.
  std::set<overflow_menu::ActionType> knownActions(
      _actionOrderData.shownActions.begin(),
      _actionOrderData.shownActions.end());
  knownActions.insert(_actionOrderData.hiddenActions.begin(),
                      _actionOrderData.hiddenActions.end());

  std::set_difference(availableActions.begin(), availableActions.end(),
                      knownActions.begin(), knownActions.end(),
                      std::back_inserter(_actionOrderData.shownActions));

  [self flushActionsToPrefs];
}

// Uses the current `destinationProvider` to get the initial order of
// destinations for new users without an ordering.
- (void)initializeDestinationOrderDataIfEmpty {
  if (_destinationOrderData.empty()) {
    _destinationOrderData.shownDestinations =
        [self.destinationProvider baseDestinations];
  }
}

// Returns the current pageActions in order.
- (NSArray<OverflowMenuAction*>*)pageActions {
  if (!IsOverflowMenuCustomizationEnabled()) {
    ActionRanking availableActions = [self.actionProvider basePageActions];
    // Convert back to Objective-C array for returning. This step also filters
    // out any actions that are not supported on the current page.
    NSMutableArray<OverflowMenuAction*>* sortedActions =
        [[NSMutableArray alloc] init];
    for (overflow_menu::ActionType action : availableActions) {
      if (OverflowMenuAction* overflowMenuAction =
              [self.actionProvider actionForActionType:action]) {
        [sortedActions addObject:overflowMenuAction];
      }
    }

    return sortedActions;
  }

  [self updateActionOrderData];

  // Convert back to Objective-C array for returning. This step also filters out
  // any actions that are not supported on the current page.
  NSMutableArray<OverflowMenuAction*>* sortedActions =
      [[NSMutableArray alloc] init];
  for (overflow_menu::ActionType action : _actionOrderData.shownActions) {
    if (OverflowMenuAction* overflowMenuAction =
            [self.actionProvider actionForActionType:action]) {
      [sortedActions addObject:overflowMenuAction];
    }
  }

  return sortedActions;
}

// Converts the current `_destinationOrderData` into an array of actual
// `OverflowMenuDestination` objects.
- (NSArray<OverflowMenuDestination*>*)destinationsFromCurrentRanking {
  // Convert back to Objective-C array for returning. This step also filters out
  // any destinations that are not supported on the current page.
  NSMutableArray<OverflowMenuDestination*>* sortedDestinations =
      [[NSMutableArray alloc] init];

  // Manually inject spotlight destination if it's supported.
  if (experimental_flags::IsSpotlightDebuggingEnabled()) {
    if (OverflowMenuDestination* spotlightDestination =
            [self.destinationProvider
                destinationForDestinationType:overflow_menu::Destination::
                                                  SpotlightDebugger]) {
      [sortedDestinations addObject:spotlightDestination];
    }
  }
  for (overflow_menu::Destination destination :
       _destinationOrderData.shownDestinations) {
    OverflowMenuDestination* overflowMenuDestination =
        [self.destinationProvider destinationForDestinationType:destination];
    if (overflowMenuDestination) {
      if (IsOverflowMenuCustomizationEnabled()) {
        // If the orderer has stored badge data about this destination, the
        // badge type may need to be upgraded. However, don't replace badges
        // with less important ones. Specifically, error badges are most
        // important.
        auto it = _destinationBadgeData.find(destination);
        if (it != _destinationBadgeData.end()) {
          if (it->second.badgeType == BadgeTypeError ||
              overflowMenuDestination.badge == BadgeTypeNone) {
            overflowMenuDestination.badge = it->second.badgeType;
          }
        }
      }
      [sortedDestinations addObject:overflowMenuDestination];
    }
  }

  return sortedDestinations;
}

#pragma mark - Badging Helpers

// Rerank the destinations, handling any badges and new items, using the new
// rules introduced during the customization project.
- (DestinationRanking)customizationRankingAfterBadgingWithAvailableDestinations:
    (DestinationRanking)availableDestinations {
  // First, update the stored badge data.
  [self updateBadgeDataWithAvailableDestinations:availableDestinations];

  // Make sure all destinations that should end up in the final ranking do.
  std::set<overflow_menu::Destination> remainingDestinations =
      [self shownDestinationsFromAvailableDestinations:availableDestinations];

  DestinationRanking newDestinationRanking;

  // Start by adding all items that don't have new positions. This is items with
  // no badge and items that appear in the first few spots already.
  for (overflow_menu::Destination destination :
       _destinationOrderData.shownDestinations) {
    if (!remainingDestinations.contains(destination)) {
      continue;
    }

    // Initial items are always added to the ranking, regardless of badge state.
    if (newDestinationRanking.size() < kNewDestinationsInsertionIndex) {
      InsertDestination(destination, remainingDestinations,
                        newDestinationRanking, true);
      continue;
    }

    // If item is badged with impressions remaining, it should be reordered to
    // a specific position and will be added later.
    if (base::Contains(_destinationBadgeData, destination) &&
        _destinationBadgeData[destination].impressionsRemaining > 0) {
      continue;
    }

    InsertDestination(destination, remainingDestinations, newDestinationRanking,
                      true);
  }

  // Iterate over the list of destinations with badges three times, inserting
  // them into the ranking. First, add items with new badges that aren't feature
  // driven, then add items with new badges that are feature driven, then add
  // items with error badges, so the final order is: error badges -> new feature
  // driven badges -> new items.
  for (auto& pair : _destinationBadgeData) {
    if (!remainingDestinations.contains(pair.first)) {
      continue;
    }

    if (pair.second.isFeatureDrivenBadge) {
      continue;
    }

    if (pair.second.badgeType == BadgeTypeNew ||
        pair.second.badgeType == BadgeTypePromo) {
      InsertDestination(pair.first, remainingDestinations,
                        newDestinationRanking, false);
    }
  }
  for (auto& pair : _destinationBadgeData) {
    if (!remainingDestinations.contains(pair.first)) {
      continue;
    }

    if (pair.second.badgeType == BadgeTypeNew ||
        pair.second.badgeType == BadgeTypePromo) {
      InsertDestination(pair.first, remainingDestinations,
                        newDestinationRanking, false);
    }
  }
  for (auto& pair : _destinationBadgeData) {
    if (!remainingDestinations.contains(pair.first)) {
      continue;
    }

    InsertDestination(pair.first, remainingDestinations, newDestinationRanking,
                      false);
  }

  // Check that all the destinations that were supposed to appear have been
  // added to the output at this point.
  DCHECK(remainingDestinations.empty());

  [self recordMetricsForBadgeReorderingWithNewDestinationRanking:
            newDestinationRanking];

  return newDestinationRanking;
}

// Modifies an updated ranking after re-ordering it based on the current badge
// status of the various destinations.
- (DestinationRanking)rankingAfterBadgingWithAvailableDestinations:
    (DestinationRanking)availableDestinations {
  DestinationRanking newDestinations =
      [self newDestinationsFromAvailableDestinations:availableDestinations];

  for (overflow_menu::Destination newDestination : newDestinations) {
    _untappedDestinations.insert(newDestination);
  }

  // Make sure that all destinations that should end up in the final ranking do.
  std::set<overflow_menu::Destination> remainingDestinations =
      [self shownDestinationsFromAvailableDestinations:availableDestinations];

  DestinationRanking sortedDestinations;

  // Reconstruct carousel based on current ranking.
  //
  // Add all ranked destinations that don't need be re-sorted back-to-back
  // following their ranking.
  //
  // Destinations that need to be re-sorted for highlight are not added here
  // where they are re-inserted later. These destinations have a badge and a
  // position of kNewDestinationsInsertionIndex or worst.
  for (overflow_menu::Destination rankedDestination :
       _destinationOrderData.shownDestinations) {
    if (remainingDestinations.contains(rankedDestination) &&
        !_untappedDestinations.contains(rankedDestination)) {
      OverflowMenuDestination* overflowMenuDestination =
          [self.destinationProvider
              destinationForDestinationType:rankedDestination];
      const bool dontSort =
          overflowMenuDestination.badge == BadgeTypeNone ||
          sortedDestinations.size() < kNewDestinationsInsertionIndex;

      if (dontSort) {
        InsertDestination(rankedDestination, remainingDestinations,
                          sortedDestinations, true);
      }
    }
  }

  // `-calculateNewRanking` excludes any
  // destinations in `_untappedDestinations` from its result, so new, untapped
  // destinations must be added to `sortedDestinations` as a separate step. New,
  // untapped destinations are inserted into the carousel starting at position
  // `kNewDestinationsInsertionIndex`. Destinations that already have a badge
  // are inserted in another step where they are inserted before the untapped
  // destinations that don't have badges.
  if (!_untappedDestinations.empty()) {
    for (overflow_menu::Destination untappedDestination :
         _untappedDestinations) {
      if (remainingDestinations.contains(untappedDestination)) {
        OverflowMenuDestination* overflowMenuDestination =
            [self.destinationProvider
                destinationForDestinationType:untappedDestination];
        if (overflowMenuDestination.badge != BadgeTypeNone) {
          continue;
        }
        overflowMenuDestination.badge = BadgeTypeNew;

        InsertDestination(untappedDestination, remainingDestinations,
                          sortedDestinations, false);
      }
    }
  }

  std::vector<overflow_menu::Destination> allDestinations;

  // Merge all destinations by prioritizing untapped destinations over ranked
  // destinations in their order of insertion.
  std::merge(_destinationOrderData.shownDestinations.begin(),
             _destinationOrderData.shownDestinations.end(),
             _untappedDestinations.begin(), _untappedDestinations.end(),
             std::back_inserter(allDestinations));

  // Insert the destinations with a badge that is not for an error at
  // kNewDestinationsInsertionIndex before the untapped destinations.
  for (overflow_menu::Destination destination : allDestinations) {
    if (remainingDestinations.contains(destination)) {
      OverflowMenuDestination* overflowMenuDestination =
          [self.destinationProvider destinationForDestinationType:destination];
      if (overflowMenuDestination.badge == BadgeTypeError) {
        continue;
      }
      InsertDestination(destination, remainingDestinations, sortedDestinations,
                        false);
    }
  }

  // Insert the destinations with an error badge before the destinations with
  // other types of badges.
  for (overflow_menu::Destination destination : allDestinations) {
    if (remainingDestinations.contains(destination) &&
        [self.destinationProvider destinationForDestinationType:destination]) {
      InsertDestination(destination, remainingDestinations, sortedDestinations,
                        false);
    }
  }

  // Check that all the destinations to show in the carousel were added to the
  // sorted destinations output at this point.
  DCHECK(remainingDestinations.empty());

  return sortedDestinations;
}

// Erases any necessary badges after the user customizes their destination list.
// This completely removes all non-feature driven badges and removes all
// impressions from feature driven ones.
- (void)eraseBadgesAfterDestinationCustomization {
  _untappedDestinations.clear();

  for (auto it = _destinationBadgeData.begin();
       it != _destinationBadgeData.end();) {
    if (it->second.isFeatureDrivenBadge) {
      it->second.impressionsRemaining = 0;
      it++;
    } else {
      it = _destinationBadgeData.erase(it);
    }
  }

  [self.destinationProvider destinationCustomizationCompleted];
}

// Detects new destinations added to the carousel by feature teams. New
// destinations (`newDestinations`) are those now found in the carousel
// (`availableDestinations`), but not found in the ranking
// (`_destinationOrderData`).
- (DestinationRanking)newDestinationsFromAvailableDestinations:
    (DestinationRanking)availableDestinations {
  std::set<overflow_menu::Destination> currentDestinations(
      availableDestinations.begin(), availableDestinations.end());

  std::set<overflow_menu::Destination> existingDestinations(
      _destinationOrderData.shownDestinations.begin(),
      _destinationOrderData.shownDestinations.end());
  existingDestinations.insert(_destinationOrderData.hiddenDestinations.begin(),
                              _destinationOrderData.hiddenDestinations.end());

  DestinationRanking newDestinations;

  std::set_difference(currentDestinations.begin(), currentDestinations.end(),
                      existingDestinations.begin(), existingDestinations.end(),
                      std::back_inserter(newDestinations));
  return newDestinations;
}

// Returns the set of destinations that should be shown, given the current
// available ones. This is different from the stored
// `_destinationOrderData.shownDestinations` because it can include new
// destinations added in code and not stored in the ranking yet.
- (std::set<overflow_menu::Destination>)
    shownDestinationsFromAvailableDestinations:
        (DestinationRanking)availableDestinations {
  std::set<overflow_menu::Destination> shownDestinations(
      availableDestinations.begin(), availableDestinations.end());

  for (overflow_menu::Destination hiddenDestination :
       _destinationOrderData.hiddenDestinations) {
    shownDestinations.erase(hiddenDestination);
  }
  return shownDestinations;
}

// Updates the stored `_destinationBadgeData`, adding any new badges due to new
// destinations or new feature-driven badges, and removing any feature-driven
// badges that are no longer present.
- (void)updateBadgeDataWithAvailableDestinations:
    (DestinationRanking)availableDestinations {
  DestinationRanking newDestinations =
      [self newDestinationsFromAvailableDestinations:availableDestinations];

  for (overflow_menu::Destination newDestination : newDestinations) {
    _untappedDestinations.insert(newDestination);

    if (!base::Contains(_destinationBadgeData, newDestination)) {
      _destinationBadgeData[newDestination].badgeType = BadgeTypeNew;
      _destinationBadgeData[newDestination].impressionsRemaining = 3;
    }
  }

  std::map<overflow_menu::Destination, BadgeType> providerBadgeTypes;

  for (overflow_menu::Destination destination : availableDestinations) {
    BadgeType badgeType =
        [self.destinationProvider destinationForDestinationType:destination]
            .badge;
    // If `destination` is hidden and has an error badge, propagate that down to
    // a visible badge.
    if (badgeType == BadgeTypeError &&
        std::find(_destinationOrderData.hiddenDestinations.begin(),
                  _destinationOrderData.hiddenDestinations.end(),
                  destination) !=
            _destinationOrderData.hiddenDestinations.end()) {
      providerBadgeTypes[overflow_menu::Destination::Settings] = BadgeTypeError;
    } else {
      // Don't override a previously propagated error badge.
      providerBadgeTypes[destination] =
          (providerBadgeTypes[destination] == BadgeTypeError) ? BadgeTypeError
                                                              : badgeType;
    }
  }

  // Make sure all badges from the provider end up in the stored badge data.
  for (const auto& [destination, badgeType] : providerBadgeTypes) {
    if (badgeType != BadgeTypeNone) {
      // If this is a new badge, the current badge is not feature driven, or the
      // badge from the provider is different than the current, then update the
      // data. Otherwise, the badge is already known about.
      if (!base::Contains(_destinationBadgeData, destination) ||
          !_destinationBadgeData[destination].isFeatureDrivenBadge ||
          badgeType != _destinationBadgeData[destination].badgeType) {
        _destinationBadgeData[destination].badgeType = badgeType;
        _destinationBadgeData[destination].impressionsRemaining = 3;
        _destinationBadgeData[destination].isFeatureDrivenBadge = true;
      }
    }
  }

  // Clear any orderer-driven badges that have finished all impressions or
  // feature-driven badges that no longer have a badge from the destination
  // provider.
  for (auto it = _destinationBadgeData.begin();
       it != _destinationBadgeData.end();) {
    if (it->second.isFeatureDrivenBadge &&
        providerBadgeTypes[it->first] == BadgeTypeNone) {
      it = _destinationBadgeData.erase(it);
    } else if (!it->second.isFeatureDrivenBadge &&
               it->second.impressionsRemaining <= 0) {
      it = _destinationBadgeData.erase(it);
    } else {
      it++;
    }
  }
}

#pragma mark - Metrics helpers

// Records any necessary metrics for when destination customization takes place,
// given the new order data the user edited.
- (void)recordMetricsForDestinationCustomizationWithNewOrderData:
    (DestinationOrderData)orderData {
  BOOL smartSortingNewlyEnabled =
      !_destinationUsageHistoryEnabled.value &&
      _destinationCustomizationModel.destinationUsageEnabled;
  BOOL smartSortingNewlyDisabled =
      _destinationUsageHistoryEnabled.value &&
      !_destinationCustomizationModel.destinationUsageEnabled;
  if (smartSortingNewlyEnabled || smartSortingNewlyDisabled) {
    IOSOverflowMenuSmartSortingChange changeType;
    if (smartSortingNewlyEnabled) {
      changeType = IOSOverflowMenuSmartSortingChange::kNewlyEnabled;
    } else {
      changeType = IOSOverflowMenuSmartSortingChange::kNewlyDisabled;
    }
    base::UmaHistogramEnumeration("IOS.OverflowMenu.SmartSortingStateChange",
                                  changeType);
  }

  std::set<overflow_menu::Destination> oldShownDestinations(
      _destinationOrderData.shownDestinations.begin(),
      _destinationOrderData.shownDestinations.end());
  std::set<overflow_menu::Destination> newShownDestinations(
      orderData.shownDestinations.begin(), orderData.shownDestinations.end());

  DestinationRanking removedDestinations;
  std::set_difference(oldShownDestinations.begin(), oldShownDestinations.end(),
                      newShownDestinations.begin(), newShownDestinations.end(),
                      std::back_inserter(removedDestinations));
  DestinationRanking addedDestinations;
  std::set_difference(newShownDestinations.begin(), newShownDestinations.end(),
                      oldShownDestinations.begin(), oldShownDestinations.end(),
                      std::back_inserter(addedDestinations));

  for (overflow_menu::Destination destination : removedDestinations) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.DestinationRemoved",
        HistogramDestinationFromDestination(destination));
  }
  for (overflow_menu::Destination destination : addedDestinations) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.DestinationAdded",
        HistogramDestinationFromDestination(destination));
  }

  if (orderData.shownDestinations.size() >= 1) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.DestinationsReordered.FirstPosition",
        HistogramDestinationFromDestination(orderData.shownDestinations[0]));
  }

  if (orderData.shownDestinations.size() >= 2) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.DestinationsReordered.SecondPosition",
        HistogramDestinationFromDestination(orderData.shownDestinations[1]));
  }

  if (orderData.shownDestinations.size() >= 3) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.DestinationsReordered.ThirdPosition",
        HistogramDestinationFromDestination(orderData.shownDestinations[2]));
  }

  if (orderData.shownDestinations.size() >= 4) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.DestinationsReordered.FourthPosition",
        HistogramDestinationFromDestination(orderData.shownDestinations[3]));
  }

  DestinationsCustomizationEvent event;
  event.PutOrRemove(
      DestinationsCustomizationEventFields::kSmartSortingTurnedOff,
      smartSortingNewlyDisabled);
  event.PutOrRemove(DestinationsCustomizationEventFields::kSmartSortingTurnedOn,
                    smartSortingNewlyEnabled);
  event.PutOrRemove(DestinationsCustomizationEventFields::kSmartSortingIsOn,
                    _destinationCustomizationModel.destinationUsageEnabled);
  event.PutOrRemove(
      DestinationsCustomizationEventFields::kDestinationWasRemoved,
      removedDestinations.size() > 0);
  event.PutOrRemove(DestinationsCustomizationEventFields::kDestinationWasAdded,
                    addedDestinations.size() > 0);

  // Loop through initial and new shown destinations lists to see if a
  // reordering occurred.
  for (unsigned long oldIndex = 0, newIndex = 0;
       oldIndex < _destinationOrderData.shownDestinations.size() &&
       newIndex < orderData.shownDestinations.size();) {
    overflow_menu::Destination oldDestination =
        _destinationOrderData.shownDestinations[oldIndex];
    overflow_menu::Destination newDestination =
        orderData.shownDestinations[newIndex];

    bool destinationRemoved =
        std::find(removedDestinations.begin(), removedDestinations.end(),
                  oldDestination) != removedDestinations.end();
    // If either destination was added or removed, skip it.
    if (destinationRemoved) {
      oldIndex++;
      continue;
    }

    bool destinationAdded =
        std::find(addedDestinations.begin(), addedDestinations.end(),
                  newDestination) != addedDestinations.end();
    if (destinationAdded) {
      newIndex++;
      continue;
    }

    if (oldDestination == newDestination) {
      oldIndex++;
      newIndex++;
      continue;
    }
    event.Put(DestinationsCustomizationEventFields::kDestinationWasReordered);
    break;
  }
  RecordDestinationsCustomizationEvent(event);
}

// Records any necessary metrics for when action customization takes place,
// given the new order data the user edited.
- (void)recordMetricsForActionCustomizationWithNewOrderData:
    (ActionOrderData)orderData {
  std::set<overflow_menu::ActionType> oldShownActions(
      _actionOrderData.shownActions.begin(),
      _actionOrderData.shownActions.end());
  std::set<overflow_menu::ActionType> newShownActions(
      orderData.shownActions.begin(), orderData.shownActions.end());

  ActionRanking removedActions;
  std::set_difference(oldShownActions.begin(), oldShownActions.end(),
                      newShownActions.begin(), newShownActions.end(),
                      std::back_inserter(removedActions));
  ActionRanking addedActions;
  std::set_difference(newShownActions.begin(), newShownActions.end(),
                      oldShownActions.begin(), oldShownActions.end(),
                      std::back_inserter(addedActions));

  for (overflow_menu::ActionType action : removedActions) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.ActionRemoved",
        HistogramActionFromActionType(action));
  }

  for (overflow_menu::ActionType action : addedActions) {
    base::UmaHistogramEnumeration("IOS.OverflowMenu.Customization.ActionAdded",
                                  HistogramActionFromActionType(action));
  }

  if (orderData.shownActions.size() >= 1) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.ActionsReordered.FirstPosition",
        HistogramActionFromActionType(orderData.shownActions[0]));
  }

  if (orderData.shownActions.size() >= 2) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.ActionsReordered.SecondPosition",
        HistogramActionFromActionType(orderData.shownActions[1]));
  }

  if (orderData.shownActions.size() >= 3) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.ActionsReordered.ThirdPosition",
        HistogramActionFromActionType(orderData.shownActions[2]));
  }

  if (orderData.shownActions.size() >= 4) {
    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.Customization.ActionsReordered.FourthPosition",
        HistogramActionFromActionType(orderData.shownActions[3]));
  }

  ActionsCustomizationEvent event;
  event.PutOrRemove(ActionsCustomizationEventFields::kActionWasRemoved,
                    removedActions.size() > 0);
  event.PutOrRemove(ActionsCustomizationEventFields::kActionWasAdded,
                    addedActions.size() > 0);
  // Loop through initial and new shown actions lists to see if a reordering
  // occurred.
  for (unsigned long oldIndex = 0, newIndex = 0;
       oldIndex < _actionOrderData.shownActions.size() &&
       newIndex < orderData.shownActions.size();) {
    overflow_menu::ActionType oldAction =
        _actionOrderData.shownActions[oldIndex];
    overflow_menu::ActionType newAction = orderData.shownActions[newIndex];

    // If either action was added or removed, skip it.
    bool actionRemoved = std::find(removedActions.begin(), removedActions.end(),
                                   oldAction) != removedActions.end();
    if (actionRemoved) {
      oldIndex++;
      continue;
    }

    bool actionAdded = std::find(addedActions.begin(), addedActions.end(),
                                 newAction) != addedActions.end();
    if (actionAdded) {
      newIndex++;
      continue;
    }

    if (oldAction == newAction) {
      oldIndex++;
      newIndex++;
    } else {
      event.Put(ActionsCustomizationEventFields::kActionWasReordered);
      break;
    }
  }
  RecordActionsCustomizationEvent(event);
}

// Records any necessary metrics for when Chrome automatically reorders the menu
// due to badges on the items.
- (void)recordMetricsForBadgeReorderingWithNewDestinationRanking:
    (DestinationRanking)newDestinationRanking {
  // Check if the menu was reordered due to a badge specifically. The assumption
  // here is that if a reordered or newly added item has a badge, then the
  // reordering was due to the badge
  BOOL reorderedDueToNewBadge = NO;
  BOOL reorderedDueToErrorBadge = NO;
  std::unordered_set<overflow_menu::Destination> movedDestinations;
  unsigned long finalIndex = 0;
  for (unsigned long initialIndex = 0;
       initialIndex < _destinationOrderData.shownDestinations.size() &&
       finalIndex < newDestinationRanking.size();) {
    overflow_menu::Destination initialDestination =
        _destinationOrderData.shownDestinations[initialIndex];
    overflow_menu::Destination finalDestination =
        newDestinationRanking[finalIndex];

    // If initial destination is not in newDestinationRanking, then it was
    // removed.
    if (std::find(newDestinationRanking.begin(), newDestinationRanking.end(),
                  initialDestination) == newDestinationRanking.end()) {
      initialIndex++;
      continue;
    }

    // If final destination is not in the current ranking, then it was added.
    if (std::find(_destinationOrderData.shownDestinations.begin(),
                  _destinationOrderData.shownDestinations.end(),
                  finalDestination) ==
        _destinationOrderData.shownDestinations.end()) {
      CHECK(_destinationBadgeData[finalDestination].badgeType != BadgeTypeNone);
      if (_destinationBadgeData[finalDestination].badgeType == BadgeTypeError) {
        reorderedDueToErrorBadge = YES;
      } else {
        reorderedDueToNewBadge = YES;
      }
      finalIndex++;
      continue;
    }

    if (initialDestination == finalDestination) {
      initialIndex++;
      finalIndex++;
    } else if (movedDestinations.contains(initialDestination)) {
      initialIndex++;
    } else {
      CHECK(_destinationBadgeData[finalDestination].badgeType != BadgeTypeNone);

      // Store that this destination was moved, so when it is encountered in the
      // intial list later on, it can be skipped over.
      movedDestinations.insert(finalDestination);
      if (_destinationBadgeData[finalDestination].badgeType == BadgeTypeError) {
        reorderedDueToErrorBadge = YES;
      } else {
        reorderedDueToNewBadge = YES;
      }

      // In this step, badges only move items forward in the list, so assume
      // that the final ordering side is the one that moved.
      finalIndex++;
    }
  }

  // Check if any remaining destinations in the final list are new.
  for (; finalIndex < newDestinationRanking.size(); finalIndex++) {
    overflow_menu::Destination finalDestination =
        newDestinationRanking[finalIndex];

    bool destinationAdded =
        std::find(_destinationOrderData.shownDestinations.begin(),
                  _destinationOrderData.shownDestinations.end(),
                  finalDestination) ==
        _destinationOrderData.shownDestinations.end();
    if (destinationAdded) {
      CHECK(_destinationBadgeData[finalDestination].badgeType != BadgeTypeNone);
      if (_destinationBadgeData[finalDestination].badgeType == BadgeTypeError) {
        reorderedDueToErrorBadge = YES;
      } else {
        reorderedDueToNewBadge = YES;
      }
      finalIndex++;
      continue;
    }
  }

  if (reorderedDueToNewBadge || reorderedDueToErrorBadge) {
    IOSOverflowMenuReorderingReason reason;
    if (reorderedDueToNewBadge && reorderedDueToErrorBadge) {
      reason = IOSOverflowMenuReorderingReason::kBothBadges;
    } else if (reorderedDueToErrorBadge) {
      reason = IOSOverflowMenuReorderingReason::kErrorBadge;
    } else {
      reason = IOSOverflowMenuReorderingReason::kNewBadge;
    }

    base::UmaHistogramEnumeration(
        "IOS.OverflowMenu.DestinationsOrderChangedProgrammatically", reason);
  }
}

@end