// Copyright 2021 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/context_menu/ui_bundled/context_menu_configuration_provider.h"

#import "base/ios/block_types.h"
#import "base/ios/ios_util.h"
#import "base/memory/weak_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros_local.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "components/policy/core/common/policy_pref_names.h"
#import "components/prefs/pref_service.h"
#import "components/search_engines/template_url_service.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider+Testing.h"
#import "ios/chrome/browser/context_menu/ui_bundled/context_menu_configuration_provider_delegate.h"
#import "ios/chrome/browser/context_menu/ui_bundled/context_menu_utils.h"
#import "ios/chrome/browser/context_menu/ui_bundled/image_preview_view_controller.h"
#import "ios/chrome/browser/enterprise/connectors/connectors_service.h"
#import "ios/chrome/browser/enterprise/connectors/connectors_service_factory.h"
#import "ios/chrome/browser/enterprise/connectors/connectors_util.h"
#import "ios/chrome/browser/enterprise/data_controls/model/data_controls_tab_helper.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/incognito_reauth/ui_bundled/incognito_reauth_commands.h"
#import "ios/chrome/browser/incognito_reauth/ui_bundled/incognito_reauth_scene_agent.h"
#import "ios/chrome/browser/intelligence/bwg/metrics/gemini_metrics.h"
#import "ios/chrome/browser/intelligence/bwg/model/gemini_service.h"
#import "ios/chrome/browser/intelligence/bwg/model/gemini_service_factory.h"
#import "ios/chrome/browser/intelligence/bwg/model/gemini_tab_helper.h"
#import "ios/chrome/browser/intelligence/bwg/utils/gemini_availability.h"
#import "ios/chrome/browser/intelligence/bwg/utils/gemini_constants.h"
#import "ios/chrome/browser/intelligence/bwg/utils/gemini_feature_availability.h"
#import "ios/chrome/browser/intelligence/features/features.h"
#import "ios/chrome/browser/lens/ui_bundled/lens_availability.h"
#import "ios/chrome/browser/lens/ui_bundled/lens_entrypoint.h"
#import "ios/chrome/browser/menu/ui_bundled/browser_action_factory.h"
#import "ios/chrome/browser/menu/ui_bundled/menu_histograms.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/photos/model/photos_availability.h"
#import "ios/chrome/browser/photos/model/photos_metrics.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/reader_mode/model/reader_mode_content_tab_helper.h"
#import "ios/chrome/browser/reader_mode/model/reader_mode_tab_helper.h"
#import "ios/chrome/browser/reading_list/model/reading_list_browser_agent.h"
#import "ios/chrome/browser/search_engines/model/search_engines_util.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.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/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/utils/mime_type_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group_utils.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/activity_service_commands.h"
#import "ios/chrome/browser/shared/public/commands/activity_service_share_url_command.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/enhanced_calendar_commands.h"
#import "ios/chrome/browser/shared/public/commands/gemini_commands.h"
#import "ios/chrome/browser/shared/public/commands/lens_commands.h"
#import "ios/chrome/browser/shared/public/commands/mini_map_commands.h"
#import "ios/chrome/browser/shared/public/commands/reading_list_add_command.h"
#import "ios/chrome/browser/shared/public/commands/scene_commands.h"
#import "ios/chrome/browser/shared/public/commands/search_image_with_lens_command.h"
#import "ios/chrome/browser/shared/public/commands/unit_conversion_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/image/image_copier.h"
#import "ios/chrome/browser/shared/ui/util/image/image_saver.h"
#import "ios/chrome/browser/shared/ui/util/pasteboard_util.h"
#import "ios/chrome/browser/shared/ui/util/url_with_title.h"
#import "ios/chrome/browser/text_selection/model/text_classifier_util.h"
#import "ios/chrome/browser/url_loading/model/image_search_param_generator.h"
#import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/chrome/browser/web/model/image_fetch/image_fetch_tab_helper.h"
#import "ios/chrome/browser/web/model/web_navigation_util.h"
#import "ios/chrome/common/ui/favicon/favicon_constants.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/components/enterprise/data_controls/clipboard_enums.h"
#import "ios/components/enterprise/data_controls/metrics_utils.h"
#import "ios/public/provider/chrome/browser/context_menu/context_menu_api.h"
#import "ios/public/provider/chrome/browser/lens/lens_api.h"
#import "ios/web/common/features.h"
#import "ios/web/common/url_scheme_util.h"
#import "ios/web/public/js_image_transcoder/java_script_image_transcoder.h"
#import "ios/web/public/ui/context_menu_params.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_delegate.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"

using data_controls::ClipboardAction;
using data_controls::ClipboardSource;
using data_controls::RecordClipboardOutcomeMetrics;
using data_controls::RecordClipboardSourceMetrics;

namespace {

// Maximum length for a context menu title formed from a URL.
const NSUInteger kContextMenuMaxURLTitleLength = 100;
// Character to append to context menut titles that are truncated.
NSString* const kContextMenuEllipsis = @"…";
// Maximum length for a context menu title formed from an address, date or phone
// number experience.
const NSUInteger kContextMenuMaxTitleLength = 30;
// Full URL alert accessibility identifier.
NSString* const kAlertAccessibilityIdentifier = @"AlertAccessibilityIdentifier";

}  // namespace

@interface ContextMenuConfigurationProvider ()

// Helper for saving images.
@property(nonatomic, strong) ImageSaver* imageSaver;
// Helper for copying images.
@property(nonatomic, strong) ImageCopier* imageCopier;

@property(nonatomic, assign) Browser* browser;

@property(nonatomic, weak) UIViewController* baseViewController;

@property(nonatomic, assign, readonly) web::WebState* webState;

@end

@implementation ContextMenuConfigurationProvider {
  /// Override the `webState` when the context menu is not triggered by the
  /// `currentWebState`.
  base::WeakPtr<web::WebState> _baseWebState;

  // Whether the context menu is presented in the lens overlay.
  BOOL _isLensOverlay;

  // Image transcoder to locally re-encode images to search.
  std::unique_ptr<web::JavaScriptImageTranscoder> _imageTranscoder;
}

- (instancetype)initWithBrowser:(Browser*)browser
             baseViewController:(UIViewController*)baseViewController
                   baseWebState:(web::WebState*)webState
                  isLensOverlay:(BOOL)isLensOverlay {
  self = [super init];
  if (self) {
    _browser = browser;
    _baseViewController = baseViewController;
    _imageSaver = [[ImageSaver alloc] initWithBrowser:self.browser];
    _imageCopier = [[ImageCopier alloc] initWithBrowser:self.browser];
    _baseWebState = webState ? webState->GetWeakPtr() : nullptr;
    _isLensOverlay = isLensOverlay;
  }
  return self;
}

- (instancetype)initWithBrowser:(Browser*)browser
             baseViewController:(UIViewController*)baseViewController {
  return [self initWithBrowser:browser
            baseViewController:baseViewController
                  baseWebState:nullptr
                 isLensOverlay:NO];
}

- (void)stop {
  _browser = nil;
  _baseViewController = nil;
  [_imageSaver stop];
  _imageSaver = nil;
  [_imageCopier stop];
  _imageCopier = nil;
  _imageTranscoder = nullptr;
  _baseWebState = nullptr;
}

- (void)dealloc {
  CHECK(!_browser);
}

- (UIContextMenuConfiguration*)
    contextMenuConfigurationForWebState:(web::WebState*)webState
                                 params:(web::ContextMenuParams)params {
  UIContextMenuContentPreviewProvider previewProvider =
      [self contextMenuContentPreviewProviderForWebState:webState
                                                  params:params];

  UIContextMenuActionProvider actionProvider =
      [self contextMenuActionProviderForWebState:webState params:params];
  if (!actionProvider) {
    return nil;
  }
  return
      [UIContextMenuConfiguration configurationWithIdentifier:nil
                                              previewProvider:previewProvider
                                               actionProvider:actionProvider];
}

- (UIMenu*)contextMenuForWebState:(web::WebState*)webState
                           params:(web::ContextMenuParams)params
                         scenario:(MenuScenarioHistogram)menuScenario {
  // Reset the URL.
  _URLToLoad = GURL();

  // Prevent context menu from displaying for a tab which is no longer the
  // current one.
  if (webState != self.webState) {
    return nil;
  }

  const GURL linkURL = params.link_url;
  const bool isLink = linkURL.is_valid();
  const GURL imageURL = params.src_url;
  const bool isImage = imageURL.is_valid();

  DCHECK(self.browser->GetProfile());
  const bool isOffTheRecord = self.browser->GetProfile()->IsOffTheRecord();

  const GURL& lastCommittedURL = webState->GetLastCommittedURL();
  web::Referrer referrer(lastCommittedURL, web::ReferrerPolicyDefault);

  NSMutableArray<UIMenuElement*>* menuElements = [[NSMutableArray alloc] init];
  NSString* menuTitle = nil;
  UIAction* showFullURL = nil;

  if (isLink || isImage) {
    menuTitle = GetContextMenuTitle(params);

    if (!IsImageTitle(params) &&
        menuTitle.length > kContextMenuMaxURLTitleLength + 1) {
      if (IsIOSWebContextMenuNewTitleEnabled()) {
        NSString* fullURL = menuTitle;
        // Truncate context menu titles that originate from URLs, leaving text
        // titles untruncated.
        menuTitle = [[menuTitle substringToIndex:kContextMenuMaxURLTitleLength]
            stringByAppendingString:kContextMenuEllipsis];
        __weak __typeof(self) weakSelf = self;
        ProceduralBlock block = ^{
          [weakSelf showFullURLPopUp:params URLString:fullURL];
        };
        ios::provider::AttachBlockToContextMenu(menuTitle, block);
      } else {
        // "Show URL action" at the top of the context menu.
        __weak __typeof(self) weakSelf = self;
        BrowserActionFactory* actionFactory =
            [[BrowserActionFactory alloc] initWithBrowser:self.browser
                                                 scenario:menuScenario];
        showFullURL = [actionFactory
            actionToShowFullURL:menuTitle
                          block:^{
                            [weakSelf showFullURLPopUp:params
                                             URLString:menuTitle];
                          }];
        menuTitle = nil;
      }
    }
  }

  if (isLink) {
    [menuElements
        addObjectsFromArray:[self contextMenuElementsForLink:linkURL
                                                    scenario:menuScenario
                                                    referrer:referrer
                                              isOffTheRecord:isOffTheRecord
                                                      params:params
                                           showFullURLAction:showFullURL]];
  }
  if (isImage) {
    [menuElements
        addObjectsFromArray:[self imageContextMenuElementsWithURL:imageURL
                                                         scenario:menuScenario
                                                         referrer:referrer
                                                   isOffTheRecord:isOffTheRecord
                                                           isLink:isLink
                                                         webState:webState
                                                           params:params]];
  }

  // This check skips every internal context menu entry. This may need to be
  // changed to only affect entity detection entries.
  if (IsEntitySelectionAllowedForURL(webState)) {
    // Insert any provided menu items. Do after Link and/or Image to allow
    // inserting at beginning or adding to end.
    ElementsToAddToContextMenu* result =
        ios::provider::GetContextMenuElementsToAdd(
            webState, params, self.baseViewController,
            HandlerForProtocol(self.browser->GetCommandDispatcher(),
                               MiniMapCommands),
            HandlerForProtocol(self.browser->GetCommandDispatcher(),
                               UnitConversionCommands),
            HandlerForProtocol(self.browser->GetCommandDispatcher(),
                               EnhancedCalendarCommands));
    if (result && result.elements) {
      [menuElements addObjectsFromArray:result.elements];
      menuTitle = result.title;
      if (menuTitle.length > kContextMenuMaxTitleLength) {
        menuTitle = [[menuTitle substringToIndex:kContextMenuMaxTitleLength - 1]
            stringByAppendingString:kContextMenuEllipsis];
      }
    }
    LOCAL_HISTOGRAM_BOOLEAN("IOS.Mobile.ContextMenu.EntitySelectionAllowed",
                            true);
  }

  if (menuElements.count == 0) {
    return nil;
  }

  return [UIMenu menuWithTitle:menuTitle children:menuElements];
}

- (void)recordMenuShown:(MenuScenarioHistogram)scenario {
  RecordMenuShown(scenario);
}

#pragma mark - Properties

- (web::WebState*)webState {
  if (_baseWebState) {
    return _baseWebState.get();
  }
  web::WebState* activeWebState =
      self.browser ? self.browser->GetWebStateList()->GetActiveWebState()
                   : nullptr;
  if (activeWebState) {
    // Check if there is an alternate webState.
    ReaderModeTabHelper* readerModeTabHelper =
        ReaderModeTabHelper::FromWebState(activeWebState);
    if (readerModeTabHelper) {
      web::WebState* readerModeWebState =
          readerModeTabHelper->GetReaderModeWebState();
      if (readerModeWebState) {
        return readerModeWebState;
      }
    }
  }
  return activeWebState;
}

#pragma mark - Private

// Returns a preview for the images in contextual menu for a given image web
// state.
- (UIContextMenuContentPreviewProvider)
    contextMenuContentPreviewProviderForWebState:(web::WebState*)webState
                                          params:
                                              (web::ContextMenuParams)params {
  if (!params.src_url.is_valid() || params.link_url.is_valid()) {
    return nil;
  }

  ImagePreviewViewController* previewViewController =
      [[ImagePreviewViewController alloc]
          initWithSrcURL:net::NSURLWithGURL(params.src_url)
                webState:webState
                 frameID:base::SysUTF8ToNSString(params.frame_id)
             frameOrigin:params.frame_security_origin];
  [previewViewController loadPreview];
  return ^() {
    return previewViewController;
  };
}

// Returns an action based contextual menu for a given web state (link, image,
// copy and intent detection actions).
- (UIContextMenuActionProvider)
    contextMenuActionProviderForWebState:(web::WebState*)webState
                                  params:(web::ContextMenuParams)params {
  const bool isLink = params.link_url.is_valid();
  const bool isImage = params.src_url.is_valid();

  // TODO(crbug.com/40823789) add scenario for not a link and not an image.
  MenuScenarioHistogram menuScenario =
      [self getMenuScenarioHistogramWithWebState:webState
                                         isImage:isImage
                                          isLink:isLink];

  UIMenu* menu = [self contextMenuForWebState:webState
                                       params:params
                                     scenario:menuScenario];
  if (menu.children.count == 0) {
    return nil;
  }
  UIContextMenuActionProvider actionProvider =
      ^(NSArray<UIMenuElement*>* suggestedActions) {
        RecordMenuShown(menuScenario);
        return menu;
      };

  return actionProvider;
}

// Returns the elements of the context menu related to links.
- (NSArray<UIMenuElement*>*)
    contextMenuElementsForLink:(GURL)linkURL
                      scenario:(MenuScenarioHistogram)scenario
                      referrer:(web::Referrer)referrer
                isOffTheRecord:(BOOL)isOffTheRecord
                        params:(web::ContextMenuParams)params
             showFullURLAction:(UIAction*)showFullURLAction {
  BrowserActionFactory* actionFactory =
      [[BrowserActionFactory alloc] initWithBrowser:self.browser
                                           scenario:scenario];

  NSMutableArray<UIMenuElement*>* linkMenuElements =
      [[NSMutableArray alloc] init];

  __weak __typeof(self) weakSelf = self;

  // Array for the actions/menus used to open a link.
  NSMutableArray<UIMenuElement*>* linkOpeningElements =
      [[NSMutableArray alloc] init];

  if (showFullURLAction) {
    [linkOpeningElements addObject:showFullURLAction];
  }

  _URLToLoad = linkURL;
  base::RecordAction(
      base::UserMetricsAction("MobileWebContextMenuLinkImpression"));
  if (web::UrlHasWebScheme(linkURL)) {
    // Open in New Tab.
    UrlLoadParams loadParams = UrlLoadParams::InNewTab(linkURL);
    loadParams.SetInBackground(YES);
    loadParams.web_params.referrer = referrer;
    loadParams.in_incognito = isOffTheRecord;
    loadParams.append_to = OpenPosition::kCurrentTab;
    loadParams.origin_point = [params.view convertPoint:params.location
                                                 toView:nil];

    UIAction* openNewTab = [actionFactory actionToOpenInNewTabWithBlock:^{
      ContextMenuConfigurationProvider* strongSelf = weakSelf;
      if (!strongSelf) {
        return;
      }
      UrlLoadingBrowserAgent::FromBrowser(strongSelf.browser)->Load(loadParams);
      [strongSelf didOpenTabInBackground:linkURL];
    }];
    [linkOpeningElements addObject:openNewTab];

    // Open in Tab Group.
    UIMenuElement* openLinkInGroupMenu =
        [self openLinkInGroupMenuElement:linkURL
                                scenario:scenario
                                referrer:referrer
                          isOffTheRecord:isOffTheRecord
                                  params:params];
    [linkOpeningElements addObject:openLinkInGroupMenu];

    if (!isOffTheRecord) {
      // Open in Incognito Tab.
      UIAction* openIncognitoTab;
      openIncognitoTab =
          [actionFactory actionToOpenInNewIncognitoTabWithURL:linkURL
                                                   completion:nil];
      [linkOpeningElements addObject:openIncognitoTab];
    }

    if (base::ios::IsMultipleScenesSupported()) {
      // Open in New Window.

      NSUserActivity* newWindowActivity = ActivityToLoadURL(
          WindowActivityContextMenuOrigin, linkURL, referrer, isOffTheRecord);
      UIAction* openNewWindow =
          [actionFactory actionToOpenInNewWindowWithActivity:newWindowActivity];

      [linkOpeningElements addObject:openNewWindow];
    }

    UIMenu* linkOpeningMenu = [UIMenu menuWithTitle:@""
                                              image:nil
                                         identifier:nil
                                            options:UIMenuOptionsDisplayInline
                                           children:linkOpeningElements];

    [linkMenuElements addObject:linkOpeningMenu];

    if (linkURL.SchemeIsHTTPOrHTTPS()) {
      NSString* innerText = params.text;
      if ([innerText length] > 0) {
        // Add to reading list.
        UIAction* addToReadingList =
            [actionFactory actionToAddToReadingListWithBlock:^{
              ContextMenuConfigurationProvider* strongSelf = weakSelf;
              if (!strongSelf) {
                return;
              }

              ReadingListAddCommand* command =
                  [[ReadingListAddCommand alloc] initWithURL:linkURL
                                                       title:innerText];
              ReadingListBrowserAgent* readingListBrowserAgent =
                  ReadingListBrowserAgent::FromBrowser(strongSelf.browser);
              readingListBrowserAgent->AddURLsToReadingList(command.URLs);
            }];
        [linkMenuElements addObject:addToReadingList];
      }
    }
  }

  // Copy Link.
  UIAction* copyLink = [actionFactory actionToCopyURLWithBlock:^{
    [weakSelf copyLinkWithURL:linkURL];
  }];
  [linkMenuElements addObject:copyLink];

  // Share Link.
  if ([self isSharingAllowed]) {
    UIAction* shareLink = [actionFactory actionToShareWithBlock:^{
      [weakSelf shareURLFromContextMenu:linkURL
                               URLTitle:params.text ? params.text : @""
                                 params:params];
    }];
    [linkMenuElements addObject:shareLink];
  }

  return linkMenuElements;
}

// Returns the elements of the context menu related to image links.
- (NSArray<UIMenuElement*>*)
    imageContextMenuElementsWithURL:(GURL)imageURL
                           scenario:(MenuScenarioHistogram)scenario
                           referrer:(web::Referrer)referrer
                     isOffTheRecord:(BOOL)isOffTheRecord
                             isLink:(BOOL)isLink
                           webState:(web::WebState*)webState
                             params:(web::ContextMenuParams)params {
  BrowserActionFactory* actionFactory =
      [[BrowserActionFactory alloc] initWithBrowser:self.browser
                                           scenario:scenario];

  NSMutableArray<UIMenuElement*>* imageMenuElements =
      [[NSMutableArray alloc] init];
  base::RecordAction(
      base::UserMetricsAction("MobileWebContextMenuImageImpression"));

  __weak __typeof(self) weakSelf = self;

  // Image saving.
  NSArray<UIMenuElement*>* imageSavingElements =
      [self imageSavingElementsWithURL:imageURL
                              scenario:scenario
                              referrer:referrer
                              webState:webState
                                params:params];
  [imageMenuElements addObjectsFromArray:imageSavingElements];

  // Copy Image.
  UIAction* copyImage = [actionFactory actionCopyImageWithBlock:^{
    [weakSelf copyImageAtURL:imageURL
                    referrer:referrer
                     frameID:params.frame_id
                 frameOrigin:params.frame_security_origin];
  }];
  [imageMenuElements addObject:copyImage];

  // Image opening.
  NSArray<UIMenuElement*>* imageOpeningElements =
      [self imageOpeningElementsWithURL:imageURL
                               scenario:scenario
                               referrer:referrer
                         isOffTheRecord:isOffTheRecord
                                 isLink:isLink
                                 params:params];
  [imageMenuElements addObjectsFromArray:imageOpeningElements];

  // Image searching.
  NSArray<UIMenuElement*>* imageSearchingElements =
      [self imageSearchingElementsWithURL:imageURL
                                 scenario:scenario
                                 referrer:referrer
                                   params:params];

  // Launch the Gemini experience with an image attached.
  UIMenuElement* geminiElement = nil;
  // To show the Gemini element, we check three distinct layers of availability:
  // - Feature-level (`IsFeatureAvailable`): Handles fine-grained or regulatory
  //   restrictions for specific features like ImageRemix, even when Gemini is
  //   generally allowed for the user.
  // - Page-specific (`IsGeminiAvailableForWebState`): Ensures the current page
  //   context can be extracted and used.
  // - Profile-level (`IsProfileEligibleForGemini`): Checks account-wide
  //   eligibility such as enterprise policies, workspace restrictions, and
  //   login state.
  BOOL canShowGeminiElement =
      gemini::IsGeminiAvailable(gemini::EntryPoint::ImageContextMenu,
                                self.browser->GetProfile(), webState)
          .enabled;
  if (canShowGeminiElement) {
    RecordImageRemixContextMenuEntryPointShown();

    ProceduralBlock geminiElementCallback = ^{
      [weakSelf openGeminiWithImageURL:imageURL
                              referrer:referrer
                                params:params];
    };
    geminiElement = [actionFactory
        actionToOpenImageInGeminiWithBlock:geminiElementCallback];
    [imageMenuElements addObject:geminiElement];
  }

  [imageMenuElements addObjectsFromArray:imageSearchingElements];

  // Share Image.
  // Shares the URL of the image and not the image itself.
  // This avoids doing in process image processing by working as the share sheet
  // fetches the image to share it.
  if (!isLink && [self isSharingAllowed]) {
    UIAction* shareImage = [actionFactory actionToShareWithBlock:^{
      [weakSelf shareURLFromContextMenu:imageURL
                               URLTitle:GetContextMenuTitle(params)
                                 params:params];
    }];
    [imageMenuElements addObject:shareImage];
  }
  return imageMenuElements;
}

// Searches an image with the given `imageURL` and `referrer`, optionally using
// Lens.
- (void)searchImageWithURL:(GURL)imageURL
                 usingLens:(BOOL)usingLens
                  referrer:(web::Referrer)referrer
                    params:(web::ContextMenuParams)params {
  ImageFetchTabHelper* imageFetcher =
      ImageFetchTabHelper::FromWebState(self.webState);
  DCHECK(imageFetcher);

  __weak ContextMenuConfigurationProvider* weakSelf = self;
  imageFetcher->GetImageData(
      imageURL, referrer, params.frame_id, params.frame_security_origin,
      ^(NSData* rawData) {
        // Arbitrary web image data requires sanitization before use.
        // TODO(b/541315801): C2PA: Web images could have C2PA metadata;
        // candidate to pass raw bytes.
        [weakSelf
            sanitizeImageData:rawData
                     mimeType:kJPEGImageMimeType
                   completion:^(NSData* transcodedData) {
                     if (usingLens) {
                       [weakSelf searchImageUsingLensWithData:transcodedData];
                     } else {
                       [weakSelf searchByImageData:transcodedData
                                          imageURL:imageURL];
                     }
                   }];
      });
}

// Sanitizes a web image data before use by passing it through the transcoder.
- (void)sanitizeImageData:(NSData*)imageData
                 mimeType:(std::string)mimeType
               completion:(void (^)(NSData*))completion {
  if (!_imageTranscoder) {
    _imageTranscoder = std::make_unique<web::JavaScriptImageTranscoder>();
  }
  _imageTranscoder->TranscodeImage(
      imageData, base::SysUTF8ToNSString(mimeType), nil, nil, nil,
      base::BindOnce(^(NSData* result, NSError* error) {
        if (error) {
          completion(nil);
          return;
        }

        completion(result);
      }));
}

// Starts a reverse image search based on `imageData` and `imageURL` in a new
// tab.
- (void)searchByImageData:(NSData*)imageData imageURL:(const GURL&)URL {
  web::NavigationManager::WebLoadParams webParams =
      ImageSearchParamGenerator::LoadParamsForImageData(
          imageData, URL,
          ios::TemplateURLServiceFactory::GetForProfile(
              self.browser->GetProfile()));
  const BOOL isIncognito = self.browser->GetProfile()->IsOffTheRecord();

  // Apply variation header data to the params.
  NSMutableDictionary<NSString*, NSString*>* combinedExtraHeaders =
      [web_navigation_util::VariationHeadersForURL(webParams.url, isIncognito)
          mutableCopy];
  [combinedExtraHeaders addEntriesFromDictionary:webParams.extra_headers];
  webParams.extra_headers = [combinedExtraHeaders copy];

  UrlLoadParams params = UrlLoadParams::InNewTab(webParams);
  params.in_incognito = isIncognito;
  UrlLoadingBrowserAgent::FromBrowser(self.browser)->Load(params);
}

// Searches an image with Lens using the given `imageData`.
- (void)searchImageUsingLensWithData:(NSData*)imageData {
  id<LensCommands> handler =
      HandlerForProtocol(_browser->GetCommandDispatcher(), LensCommands);
  UIImage* image = [UIImage imageWithData:imageData];
  SearchImageWithLensCommand* command = [[SearchImageWithLensCommand alloc]
      initWithImage:image
         entryPoint:LensEntrypoint::ContextMenu];
  [handler searchImageWithLens:command];
}

// Returns the context menu element to open a URL (image or link) in a group.
- (UIMenuElement*)openLinkInGroupMenuElement:(GURL)linkURL
                                    scenario:(MenuScenarioHistogram)scenario
                                    referrer:(web::Referrer)referrer
                              isOffTheRecord:(BOOL)isOffTheRecord
                                      params:(web::ContextMenuParams)params {
  BrowserActionFactory* actionFactory =
      [[BrowserActionFactory alloc] initWithBrowser:self.browser
                                           scenario:scenario];
  __weak __typeof(self) weakSelf = self;

  std::set<const TabGroup*> groups =
      GetAllGroupsForProfile(self.browser->GetProfile());

  auto actionResult = ^(const TabGroup* group) {
    ContextMenuConfigurationProvider* strongSelf = weakSelf;
    if (!strongSelf) {
      return;
    }
    UrlLoadParams groupLoadParams = UrlLoadParams::InNewTab(linkURL);
    groupLoadParams.SetInBackground(YES);
    groupLoadParams.web_params.referrer = referrer;
    groupLoadParams.in_incognito = isOffTheRecord;
    groupLoadParams.append_to = OpenPosition::kCurrentTab;
    groupLoadParams.origin_point = [params.view convertPoint:params.location
                                                      toView:nil];
    groupLoadParams.load_in_group = true;
    if (group) {
      groupLoadParams.tab_group = group->GetWeakPtr();
    }

    UrlLoadingBrowserAgent::FromBrowser(strongSelf.browser)
        ->Load(groupLoadParams);
    [strongSelf didOpenTabInBackground:linkURL];
  };

  return [actionFactory menuToOpenLinkInGroupWithGroups:groups
                                                  block:actionResult];
}

// Returns the context menu elements for image opening.
- (NSArray<UIMenuElement*>*)
    imageOpeningElementsWithURL:(GURL)imageURL
                       scenario:(MenuScenarioHistogram)scenario
                       referrer:(web::Referrer)referrer
                 isOffTheRecord:(BOOL)isOffTheRecord
                         isLink:(BOOL)isLink
                         params:(web::ContextMenuParams)params {
  BrowserActionFactory* actionFactory =
      [[BrowserActionFactory alloc] initWithBrowser:self.browser
                                           scenario:scenario];

  NSMutableArray<UIMenuElement*>* imageOpeningMenuElements =
      [[NSMutableArray alloc] init];

  // Open Image.
  // TODO(crbug.com/351817704): Add open image suport for lens overlay.
  if (!_isLensOverlay) {
    UIAction* openImage = [actionFactory actionOpenImageWithURL:imageURL
                                                     completion:nil];
    [imageOpeningMenuElements addObject:openImage];
  }

  // Open Image in new tab.
  UrlLoadParams loadParams = UrlLoadParams::InNewTab(imageURL);
  loadParams.SetInBackground(YES);
  loadParams.web_params.referrer = referrer;
  loadParams.in_incognito = isOffTheRecord;
  loadParams.append_to = OpenPosition::kCurrentTab;
  loadParams.origin_point = [params.view convertPoint:params.location
                                               toView:nil];

  __weak __typeof__(self) weakSelf = self;
  UIAction* openImageInNewTab = [actionFactory
      actionOpenImageInNewTabWithUrlLoadParams:loadParams
                                    completion:^() {
                                      [weakSelf
                                          didOpenTabInBackground:imageURL];
                                    }];

  // Check if the URL was a valid link to avoid having the `Open in Tab Group`
  // option twice.
  if (isLink) {
    [imageOpeningMenuElements addObject:openImageInNewTab];
  } else {
    // Array for the actions/menus used to open an image in a new tab.
    NSMutableArray<UIMenuElement*>* imageOpeningElements =
        [[NSMutableArray alloc] init];

    [imageOpeningElements addObject:openImageInNewTab];

    // Open in Tab Group.
    UIMenuElement* openLinkInGroupMenu =
        [self openLinkInGroupMenuElement:imageURL
                                scenario:scenario
                                referrer:referrer
                          isOffTheRecord:isOffTheRecord
                                  params:params];
    [imageOpeningElements addObject:openLinkInGroupMenu];

    UIMenu* imageOpeningMenu = [UIMenu menuWithTitle:@""
                                               image:nil
                                          identifier:nil
                                             options:UIMenuOptionsDisplayInline
                                            children:imageOpeningElements];

    [imageOpeningMenuElements addObject:imageOpeningMenu];
  }

  return imageOpeningMenuElements;
}

// Returns the context menu elements for image saving.
- (NSArray<UIMenuElement*>*)
    imageSavingElementsWithURL:(GURL)imageURL
                      scenario:(MenuScenarioHistogram)scenario
                      referrer:(web::Referrer)referrer
                      webState:(web::WebState*)webState
                        params:(web::ContextMenuParams)params {
  // TODO(crbug.com/351817704): Save to photo is not presented in the
  // baseViewController.
  const bool saveToPhotosAvailable =
      !_isLensOverlay && IsSaveToPhotosAvailable(self.browser->GetProfile());

  BrowserActionFactory* actionFactory =
      [[BrowserActionFactory alloc] initWithBrowser:self.browser
                                           scenario:scenario];

  NSMutableArray<UIMenuElement*>* imageSavingElements =
      [[NSMutableArray alloc] init];

  __weak __typeof(self) weakSelf = self;

  // Save Image.
  UIAction* saveImage = [actionFactory actionSaveImageWithBlock:^{
    ContextMenuConfigurationProvider* strongSelf = weakSelf;
    if (!strongSelf || !strongSelf.baseViewController) {
      return;
    }
    [strongSelf.imageSaver saveImageAtURL:imageURL
                                 referrer:referrer
                                 webState:strongSelf.webState
                                  frameID:params.frame_id
                              frameOrigin:params.frame_security_origin
                       baseViewController:strongSelf.baseViewController];
    base::UmaHistogramEnumeration(
        kSaveToPhotosContextMenuActionsHistogram,
        saveToPhotosAvailable
            ? SaveToPhotosContextMenuActions::kAvailableDidSaveImageLocally
            : SaveToPhotosContextMenuActions::kUnavailableDidSaveImageLocally);
  }];

  auto* service =
      enterprise_connectors::ConnectorsServiceFactory::GetForProfile(
          self.browser->GetProfile());
  bool downloadConnectorEnabled =
      enterprise_connectors::IsDownloadConnectorEnabled(service);
  policy::DownloadRestriction download_restriction =
      static_cast<policy::DownloadRestriction>(
          self.browser->GetProfile()->GetPrefs()->GetInteger(
              policy::policy_prefs::kDownloadRestrictions));
  if (download_restriction == policy::DownloadRestriction::ALL_FILES ||
      downloadConnectorEnabled) {
    saveImage.subtitle =
        l10n_util::GetNSString(IDS_POLICY_ACTION_BLOCKED_BY_ORGANIZATION);
    saveImage.attributes = UIMenuElementAttributesDisabled;
  }

  [imageSavingElements addObject:saveImage];

  if (!saveToPhotosAvailable) {
    return imageSavingElements;
  }

  // Save Image to Photos.
  base::RecordAction(base::UserMetricsAction(
      "MobileWebContextMenuImageWithSaveToPhotosImpression"));
  UIAction* saveImageToPhotosAction = [actionFactory
      actionToSaveToPhotosWithImageURL:imageURL
                              referrer:referrer
                              webState:webState
                                params:params
                                 block:^{
                                   base::UmaHistogramEnumeration(
                                       kSaveToPhotosContextMenuActionsHistogram,
                                       SaveToPhotosContextMenuActions::
                                           kAvailableDidSaveImageToGooglePhotos);
                                 }];

  if (downloadConnectorEnabled) {
    saveImageToPhotosAction.subtitle =
        l10n_util::GetNSString(IDS_POLICY_ACTION_BLOCKED_BY_ORGANIZATION);
    saveImageToPhotosAction.attributes = UIMenuElementAttributesDisabled;
  }
  [imageSavingElements addObject:saveImageToPhotosAction];

  // Save Image Menu.
  UIImage* image =
      SymbolWithPointSize(SymbolPhotoBadgeArrowDown, kSymbolActionPointSize);
  UIMenu* saveImageInMenu = [UIMenu
      menuWithTitle:l10n_util::GetNSString(IDS_IOS_TOOLS_MENU_SAVE_IMAGE_IN)
              image:image
         identifier:nil
            options:UIMenuOptionsSingleSelection
           children:imageSavingElements];
  return @[ saveImageInMenu ];
}

// Returns the context menu elements for image searching.
- (NSArray<UIMenuElement*>*)
    imageSearchingElementsWithURL:(GURL)imageURL
                         scenario:(MenuScenarioHistogram)scenario
                         referrer:(web::Referrer)referrer
                           params:(web::ContextMenuParams)params {
  if (_isLensOverlay) {
    return @[];
  }
  BrowserActionFactory* actionFactory =
      [[BrowserActionFactory alloc] initWithBrowser:self.browser
                                           scenario:scenario];

  __weak __typeof(self) weakSelf = self;

  // Search the image using Lens if Lens is enabled and available. Otherwise
  // fall back to a standard search by image experience.
  TemplateURLService* service =
      ios::TemplateURLServiceFactory::GetForProfile(self.browser->GetProfile());

  const BOOL useLens =
      lens_availability::CheckAndLogAvailabilityForLensEntryPoint(
          LensEntrypoint::ContextMenu,
          search_engines::SupportsSearchImageWithLens(service));

  NSMutableArray<UIMenuElement*>* imageSearchingMenuElements =
      [[NSMutableArray alloc] init];
  if (useLens) {
    UIAction* searchImageWithLensAction =
        [actionFactory actionToSearchImageUsingLensWithBlock:^{
          [weakSelf searchImageWithURL:imageURL
                             usingLens:YES
                              referrer:referrer
                                params:params];
        }];
    [imageSearchingMenuElements addObject:searchImageWithLensAction];
  }

  if (!useLens && search_engines::SupportsSearchByImage(service)) {
    const TemplateURL* defaultURL = service->GetDefaultSearchProvider();
    NSString* title = l10n_util::GetNSStringF(
        IDS_IOS_CONTEXT_MENU_SEARCHWEBFORIMAGE, defaultURL->short_name());
    UIAction* searchByImage = [actionFactory
        actionSearchImageWithTitle:title
                             Block:^{
                               [weakSelf searchImageWithURL:imageURL
                                                  usingLens:NO
                                                   referrer:referrer
                                                     params:params];
                             }];
    [imageSearchingMenuElements addObject:searchByImage];
  }

  return imageSearchingMenuElements;
}

// Creates the UIAlertController with URLString that appears when clicking
// on Show Full URL button from the context menu.
- (void)showFullURLPopUp:(web::ContextMenuParams)params
               URLString:(NSString*)URLString {
  // Due to a UIKit bug, UIAlertController that show a URL may truncate their
  // last line. To avoid masking useful information, add an artificial empty
  // last line that can be truncated safely.
  // The "..." will still be visible but no useful information will be lost.
  // TODO(crbug.com/407565099): remove workaround.
  UIAlertController* alert = [UIAlertController
      alertControllerWithTitle:@""
                       message:[URLString
                                   stringByAppendingString:@"\u00a0\n\u00a0"]
                preferredStyle:UIAlertControllerStyleAlert];

  UIAlertAction* defaultAction = [UIAlertAction
      actionWithTitle:l10n_util::GetNSString(IDS_IOS_CLOSE_ALERT_BUTTON_LABEL)
                style:UIAlertActionStyleDefault
              handler:nil];

  [alert addAction:defaultAction];
  alert.view.accessibilityIdentifier = kAlertAccessibilityIdentifier;
  // The alert pop up will be presenting on top of the menu.
  [self.baseViewController.presentedViewController presentViewController:alert
                                                                animated:YES
                                                              completion:nil];
}

// Calls the `-showShareSheetForURL:` command with the given
// `ActivityServiceShareURLCommand` command.
- (void)shareURLFromContextMenu:(const GURL&)URLToShare
                       URLTitle:(NSString*)URLTitle
                         params:(web::ContextMenuParams)params {
  id<ActivityServiceCommands> handler = HandlerForProtocol(
      _browser->GetCommandDispatcher(), ActivityServiceCommands);

  CGRect sourceRect = CGRectMake(params.location.x, params.location.y, 0, 0);

  ActivityServiceShareURLCommand* command =
      [[ActivityServiceShareURLCommand alloc] initWithURL:URLToShare
                                                    title:URLTitle
                                               sourceView:params.view
                                               sourceRect:sourceRect];
  [handler showShareSheetForURL:command];
}

// Informs the delegate that a new tab has been opened in the background.
- (void)didOpenTabInBackground:(GURL)URL {
  [self.delegate contextMenuConfigurationProvider:self
                 didOpenNewTabInBackgroundWithURL:URL];
}

- (MenuScenarioHistogram)getMenuScenarioHistogramWithWebState:
                             (web::WebState*)webState
                                                      isImage:(BOOL)isImage
                                                       isLink:(BOOL)isLink {
  // Check that the Reader Mode web state implicitly checking that the content
  // tab helper is attached.
  ReaderModeContentTabHelper* readerModeContentTabHelper =
      ReaderModeContentTabHelper::FromWebState(webState);
  BOOL isReaderModeActive = readerModeContentTabHelper;
  if (isReaderModeActive) {
    if (isImage && isLink) {
      return kMenuScenarioHistogramReaderModeContextMenuImageLink;
    } else if (isImage) {
      return kMenuScenarioHistogramReaderModeContextMenuImage;
    } else {
      return kMenuScenarioHistogramReaderModeContextMenuLink;
    }
  } else {
    if (isImage && isLink) {
      return kMenuScenarioHistogramContextMenuImageLink;
    } else if (isImage) {
      return kMenuScenarioHistogramContextMenuImage;
    } else {
      return kMenuScenarioHistogramContextMenuLink;
    }
  }
}

- (void)copyImageAtURL:(GURL)imageURL
              referrer:(web::Referrer)referrer
               frameID:(const std::string&)frameID
           frameOrigin:(const url::Origin&)frameOrigin {
  if (!self.webState) {
    return;
  }

  RecordClipboardSourceMetrics(ClipboardAction::kCopy,
                               ClipboardSource::kCustomAction);

  const std::string frameIDCopy = frameID;
  const url::Origin frameOriginCopy = frameOrigin;

  __weak __typeof(self) weakSelf = self;
  ProceduralBlock finishCopyImage = ^{
    ContextMenuConfigurationProvider* strongSelf = weakSelf;
    if (!strongSelf || !strongSelf.baseViewController) {
      return;
    }
    [weakSelf.imageCopier copyImageAtURL:imageURL
                                referrer:referrer
                                webState:strongSelf.webState
                                 frameID:frameIDCopy
                             frameOrigin:frameOriginCopy
                      baseViewController:strongSelf.baseViewController];
  };

  web::WebStateDelegate* webStateDelegate = self.webState->GetDelegate();
  if (!webStateDelegate) {
    RecordClipboardOutcomeMetrics(ClipboardAction::kCopy, true);
    finishCopyImage();
    return;
  }

  // Check if copying content from the current web page is allowed by
  // policies.
  webStateDelegate->ShouldAllowCopy(
      self.webState, base::BindOnce(^(bool allowed) {
        RecordClipboardOutcomeMetrics(ClipboardAction::kCopy, allowed);
        if (allowed) {
          finishCopyImage();
        }
      }));
}

// Checks enterprise policy and copies the given `linkURL` to the pasteboard.
- (void)copyLinkWithURL:(GURL)linkURL {
  if (!self.webState) {
    return;
  }

  RecordClipboardSourceMetrics(ClipboardAction::kCopy,
                               ClipboardSource::kCustomAction);

  web::WebStateDelegate* webStateDelegate = self.webState->GetDelegate();
  if (!webStateDelegate) {
    RecordClipboardOutcomeMetrics(ClipboardAction::kCopy, true);
    StoreURLInPasteboard(linkURL);
    return;
  }

  // Check if copying content from the current web page is allowed by
  // policies.
  webStateDelegate->ShouldAllowCopy(
      self.webState, base::BindOnce(^(bool allowed) {
        RecordClipboardOutcomeMetrics(ClipboardAction::kCopy, allowed);
        if (allowed) {
          StoreURLInPasteboard(linkURL);
        }
      }));
}

// Returns true if sharing from the context menu is allowed.
- (BOOL)isSharingAllowed {
  // TODO(crbug.com/351817704): Disable the share menu with lens overlay as the
  // share sheet is not presented in `baseViewController`.
  if (_isLensOverlay) {
    return NO;
  }

  auto* data_controls_tab_helper =
      data_controls::DataControlsTabHelper::FromWebState(self.webState);
  return data_controls_tab_helper->ShouldAllowShare();
}

// Opens the Gemini overlay with an image attached. Fetches the image from
// `imageURL` using `referrer`, and then sanitizes/transcodes the image.
- (void)openGeminiWithImageURL:(GURL)imageURL
                      referrer:(web::Referrer)referrer
                        params:(web::ContextMenuParams)params {
  ImageFetchTabHelper* imageFetcher =
      ImageFetchTabHelper::FromWebState(self.webState);
  CHECK(imageFetcher);

  __weak ContextMenuConfigurationProvider* weakSelf = self;
  imageFetcher->GetImageData(
      imageURL, referrer, params.frame_id, params.frame_security_origin,
      ^(NSData* imageData) {
        // Safely transcode image data.
        [weakSelf sanitizeImageData:imageData
                           mimeType:kPortableNetworkGraphicMimeType
                         completion:^(NSData* transcodedData) {
                           UIImage* imageFromData = nil;
                           if (transcodedData) {
                             imageFromData =
                                 [UIImage imageWithData:transcodedData];
                           }

                           [weakSelf openGeminiWithImage:imageFromData];
                         }];
      });
}

// Opens the Gemini overlay with an image attached. The sanitized `image` is
// passed to Gemini.
- (void)openGeminiWithImage:(UIImage*)image {
  double aspectRatio = 0.0;
  if (image.size.width > 0 && image.size.height > 0) {
    aspectRatio = image.size.width / image.size.height;
  }
  RecordImageRemixContextMenuEntryPointTapped(aspectRatio);

  id<GeminiCommands> handler =
      HandlerForProtocol(_browser->GetCommandDispatcher(), GeminiCommands);
  GeminiStartupState* state = [[GeminiStartupState alloc]
      initWithEntryPoint:gemini::EntryPoint::ImageContextMenu];
  state.imageAttachment = image;
  [handler startGeminiEntryFlowWithStartupState:state
                             baseViewController:self.baseViewController
                       showSnackbarOnCompletion:YES
                                     completion:nil];
}

@end
