// Copyright 2012 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/shared/ui/util/uikit_ui_util.h"

#import <Accelerate/Accelerate.h>
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>
#import <UIKit/UIKit.h>
#import <stddef.h>
#import <stdint.h>

#import <cmath>

#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/check_op.h"
#import "base/ios/ios_util.h"
#import "base/notreached.h"
#import "base/numerics/math_constants.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/web/public/thread/web_task_traits.h"
#import "ios/web/public/thread/web_thread.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/base/resource/resource_bundle.h"
#import "ui/gfx/scoped_cg_context_save_gstate_mac.h"

void SetA11yLabelAndUiAutomationName(
    NSObject<UIAccessibilityIdentification>* element,
    int idsAccessibilityLabel,
    NSString* englishUiAutomationName) {
  [element setAccessibilityLabel:l10n_util::GetNSString(idsAccessibilityLabel)];
  [element setAccessibilityIdentifier:englishUiAutomationName];
}

void SetUILabelScaledFont(UILabel* label, UIFont* font) {
  label.font = [[UIFontMetrics defaultMetrics] scaledFontForFont:font];
  label.adjustsFontForContentSizeCategory = YES;
}

void MaybeSetUILabelScaledFont(BOOL maybe, UILabel* label, UIFont* font) {
  if (maybe) {
    SetUILabelScaledFont(label, font);
  } else {
    label.font = font;
  }
}

UIFont* PreferredFontForTextStyle(UIFontTextStyle style,
                                  std::optional<UIFontWeight> weight,
                                  std::optional<CGFloat> max_size) {
  // Fallback to using the simpler method if no weight or max_size was given.
  if (!weight.has_value() && !max_size.has_value()) {
    return [UIFont preferredFontForTextStyle:style];
  }

  // Get a "base font", with the given style and weight.
  UITraitCollection* default_traits =
      [UITraitCollection traitCollectionWithPreferredContentSizeCategory:
                             UIContentSizeCategoryLarge];
  UIFont* base_font;
  if (weight.has_value()) {
    UIFontDescriptor* descriptor =
        [UIFontDescriptor preferredFontDescriptorWithTextStyle:style
                                 compatibleWithTraitCollection:default_traits];
    base_font = [UIFont systemFontOfSize:descriptor.pointSize
                                  weight:weight.value()];
  } else {
    base_font = [UIFont preferredFontForTextStyle:style
                    compatibleWithTraitCollection:default_traits];
  }

  // Return a scaled version of the base font, that can be adjusted
  // automatically by setting adjustsFontForContentSizeCategory.
  UIFontMetrics* metrics = [UIFontMetrics metricsForTextStyle:style];
  if (max_size.has_value()) {
    return [metrics scaledFontForFont:base_font
                     maximumPointSize:max_size.value()];
  }
  return [metrics scaledFontForFont:base_font];
}

void SetUITextFieldScaledFont(UITextField* textField, UIFont* font) {
  textField.font = [[UIFontMetrics defaultMetrics] scaledFontForFont:font];
  textField.adjustsFontForContentSizeCategory = YES;
}

void MaybeSetUITextFieldScaledFont(BOOL maybe,
                                   UITextField* textField,
                                   UIFont* font) {
  if (maybe) {
    SetUITextFieldScaledFont(textField, font);
  } else {
    textField.font = font;
  }
}

UIFont* CreateDynamicFont(UIFontTextStyle style, UIFontWeight weight) {
  UIFontDescriptor* fontDescriptor =
      [UIFontDescriptor preferredFontDescriptorWithTextStyle:style];
  return [UIFont systemFontOfSize:fontDescriptor.pointSize weight:weight];
}

UIFont* CreateDynamicFont(UIFontTextStyle style,
                          UIFontWeight weight,
                          id<UITraitEnvironment> environment) {
  UIFontDescriptor* fontDescriptor = [UIFontDescriptor
      preferredFontDescriptorWithTextStyle:style
             compatibleWithTraitCollection:environment.traitCollection];
  return [UIFont systemFontOfSize:fontDescriptor.pointSize weight:weight];
}

UIImage* CaptureViewWithOption(UIView* view,
                               CGFloat scale,
                               CaptureViewOption option) {
  UIGraphicsImageRendererFormat* format =
      [UIGraphicsImageRendererFormat preferredFormat];
  format.scale = scale;
  format.opaque = NO;
  UIGraphicsImageRenderer* renderer =
      [[UIGraphicsImageRenderer alloc] initWithSize:view.bounds.size
                                             format:format];

  return [renderer imageWithActions:^(UIGraphicsImageRendererContext* context) {
    [view drawViewHierarchyInRect:view.bounds
               afterScreenUpdates:option == kClientSideRendering];
  }];
}

UIImage* CaptureView(UIView* view, CGFloat scale) {
  return CaptureViewWithOption(view, scale, kNoCaptureOption);
}

UIImage* GreyImage(UIImage* image) {
  DCHECK(image);
  UIGraphicsImageRendererFormat* format =
      [UIGraphicsImageRendererFormat preferredFormat];
  // Grey images are always non-retina to improve memory performance.
  format.scale = 1;
  format.opaque = YES;
  CGRect greyImageRect = CGRectMake(0, 0, image.size.width, image.size.height);
  UIGraphicsImageRenderer* renderer =
      [[UIGraphicsImageRenderer alloc] initWithSize:greyImageRect.size
                                             format:format];
  return [renderer imageWithActions:^(UIGraphicsImageRendererContext* context) {
    UIBezierPath* background = [UIBezierPath bezierPathWithRect:greyImageRect];
    [UIColor.blackColor set];
    [background fill];

    [image drawInRect:greyImageRect blendMode:kCGBlendModeLuminosity alpha:1.0];
  }];
}

UIImage* NativeReversibleImage(int imageID, BOOL reversible) {
  DCHECK(imageID);
  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
  UIImage* image = rb.GetNativeImageNamed(imageID).ToUIImage();
  return (reversible && UseRTLLayout())
             ? [image imageFlippedForRightToLeftLayoutDirection]
             : image;
}

UIImage* NativeImage(int imageID) {
  return NativeReversibleImage(imageID, NO);
}

UIInterfaceOrientation GetInterfaceOrientation(UIWindow* window) {
  return window.windowScene.effectiveGeometry.interfaceOrientation;
}

UIActivityIndicatorView* GetMediumUIActivityIndicatorView() {
  return [[UIActivityIndicatorView alloc]
      initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium];
}

UIActivityIndicatorView* GetLargeUIActivityIndicatorView() {
  return [[UIActivityIndicatorView alloc]
      initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleLarge];
}

CGFloat CurrentKeyboardHeight(NSValue* keyboardFrameValue) {
  return [keyboardFrameValue CGRectValue].size.height;
}

CGFloat VisibleKeyboardHeightFromNotification(NSNotification* notification,
                                              UIWindow* window) {
  if (!window) {
    return 0;
  }
  NSDictionary* user_info = notification.userInfo;
  CGRect keyboard_frame =
      [user_info[UIKeyboardFrameEndUserInfoKey] CGRectValue];

  id<UICoordinateSpace> from_coordinate_space =
      ((UIScreen*)notification.object).coordinateSpace;

  CGRect keyboard_frame_in_window =
      [from_coordinate_space convertRect:keyboard_frame
                       toCoordinateSpace:window.coordinateSpace];

  return CGRectIntersection(keyboard_frame_in_window, window.bounds)
      .size.height;
}

UIImage* ImageWithColor(UIColor* color) {
  CGRect rect = CGRectMake(0, 0, 1, 1);
  UIGraphicsImageRenderer* renderer =
      [[UIGraphicsImageRenderer alloc] initWithSize:rect.size];
  return [renderer imageWithActions:^(UIGraphicsImageRendererContext* context) {
    [color setFill];
    [context fillRect:rect];
  }];
}

UIImage* CircularImageFromImage(UIImage* image, CGFloat width) {
  CGRect frame =
      CGRectMakeAlignedAndCenteredAt(width / 2.0, width / 2.0, width);

  UIGraphicsImageRendererFormat* format =
      [UIGraphicsImageRendererFormat preferredFormat];
  format.opaque = NO;

  UIGraphicsImageRenderer* renderer =
      [[UIGraphicsImageRenderer alloc] initWithSize:frame.size format:format];
  return
      [renderer imageWithActions:^(UIGraphicsImageRendererContext* UIContext) {
        CGContextRef context = UIContext.CGContext;
        CGContextBeginPath(context);
        CGContextAddEllipseInRect(context, frame);
        CGContextClosePath(context);
        CGContextClip(context);

        CGFloat scaleX = frame.size.width / image.size.width;
        CGFloat scaleY = frame.size.height / image.size.height;
        CGFloat scale = std::max(scaleX, scaleY);
        CGContextScaleCTM(context, scale, scale);

        [image
            drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
      }];
}

UIImage* ImageWithCornerRadius(UIImage* image, CGFloat cornerRadius) {
  CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);

  UIGraphicsImageRenderer* renderer =
      [[UIGraphicsImageRenderer alloc] initWithSize:rect.size];

  UIImage* roundedImage = [renderer
      imageWithActions:^(UIGraphicsImageRendererContext* rendererContext) {
        UIBezierPath* path =
            [UIBezierPath bezierPathWithRoundedRect:rect
                                       cornerRadius:cornerRadius];
        [path addClip];
        [image drawInRect:rect];
      }];
  return roundedImage;
}

bool IsPortrait(UIWindow* window) {
  UIInterfaceOrientation orient = GetInterfaceOrientation(window);
  return UIInterfaceOrientationIsPortrait(orient) ||
         orient == UIInterfaceOrientationUnknown;
}

bool IsLandscape(UIWindow* window) {
  return UIInterfaceOrientationIsLandscape(GetInterfaceOrientation(window));
}

bool IsWindowedMode(UIWindow* window) {
  if (!window) {
    return false;
  }
  UIWindowScene* scene = window.windowScene;
  if (!scene) {
    return false;
  }
  return !CGRectEqualToRect(window.bounds, scene.screen.bounds);
}

bool CanShowTabStrip(UITraitCollection* traitCollection) {
  if (IsRegularXRegularSizeClass(traitCollection)) {
    return true;
  }
  if (@available(iOS 26, *)) {
    return ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET;
  }

  return false;
}

bool CanShowTabStrip(id<UITraitEnvironment> environment) {
  return CanShowTabStrip(environment.traitCollection);
}

bool IsIPhoneLandscape(id<UITraitEnvironment> environment) {
  return IsIPhoneLandscape(environment.traitCollection);
}

bool IsIPhoneLandscape(UITraitCollection* trait_collection) {
  return IsCompactHeight(trait_collection);
}

bool IsCompactWidth(id<UITraitEnvironment> environment) {
  return IsCompactWidth(environment.traitCollection);
}

bool IsCompactWidth(UITraitCollection* traitCollection) {
  return traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact;
}

bool IsCompactHeight(id<UITraitEnvironment> environment) {
  return IsCompactHeight(environment.traitCollection);
}

bool IsCompactHeight(UITraitCollection* traitCollection) {
  return traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact;
}

bool ShouldShowCompactToolbar(id<UITraitEnvironment> environment) {
  return ShouldShowCompactToolbar(environment.traitCollection);
}

bool ShouldShowCompactToolbar(UITraitCollection* traitCollection) {
  return !IsRegularXRegularSizeClass(traitCollection);
}

bool IsSplitToolbarMode(id<UITraitEnvironment> environment) {
  return IsSplitToolbarMode(environment.traitCollection);
}

bool IsSplitToolbarMode(UITraitCollection* traitCollection) {
  return IsCompactWidth(traitCollection) && !IsCompactHeight(traitCollection);
}

UIView* GetFirstResponderSubview(UIView* view) {
  if ([view isFirstResponder]) {
    return view;
  }

  for (UIView* subview in [view subviews]) {
    UIView* firstResponder = GetFirstResponderSubview(subview);
    if (firstResponder) {
      return firstResponder;
    }
  }

  return nil;
}

UIResponder* GetFirstResponder() {
  DCHECK(NSThread.isMainThread);
  return GetFirstResponderSubview(GetAnyKeyWindow());
}

UIResponder* GetFirstResponderInWindowScene(UIWindowScene* windowScene) {
  DCHECK(NSThread.isMainThread);
  if (!windowScene) {
    return nil;
  }

  // First checking the key window for this window scene.
  UIResponder* responder = GetFirstResponderSubview(windowScene.keyWindow);
  if (responder) {
    return responder;
  }

  for (UIWindow* window in windowScene.windows) {
    if (window.keyWindow) {
      continue;
    }
    responder = GetFirstResponderSubview(window);
    if (responder) {
      return responder;
    }
  }

  return nil;
}

// Trigger a haptic vibration for the user selecting an action. This is a no-op
// for devices that do not support it.
void TriggerHapticFeedbackForImpact(UIImpactFeedbackStyle impactStyle) {
  // Although Apple documentation claims that UIFeedbackGenerator and its
  // concrete subclasses are available on iOS 10+, they are not really
  // available on an app whose deployment target is iOS 10.0 (iOS 10.1+ are
  // okay) and Chrome will fail at dynamic link time and instantly crash.
  // NSClassFromString() checks if Objective-C run-time has the class before
  // using it.
  Class generatorClass = NSClassFromString(@"UIImpactFeedbackGenerator");
  if (generatorClass) {
    UIImpactFeedbackGenerator* generator =
        [[generatorClass alloc] initWithStyle:impactStyle];
    [generator impactOccurred];
  }
}

// Trigger a haptic vibration for the change in selection. This is a no-op for
// devices that do not support it.
void TriggerHapticFeedbackForSelectionChange() {
  // Although Apple documentation claims that UIFeedbackGenerator and its
  // concrete subclasses are available on iOS 10+, they are not really
  // available on an app whose deployment target is iOS 10.0 (iOS 10.1+ are
  // okay) and Chrome will fail at dynamic link time and instantly crash.
  // NSClassFromString() checks if Objective-C run-time has the class before
  // using it.
  Class generatorClass = NSClassFromString(@"UISelectionFeedbackGenerator");
  if (generatorClass) {
    UISelectionFeedbackGenerator* generator = [[generatorClass alloc] init];
    [generator selectionChanged];
  }
}

// Trigger a haptic vibration for a notification. This is a no-op for devices
// that do not support it.
void TriggerHapticFeedbackForNotification(UINotificationFeedbackType type) {
  // Although Apple documentation claims that UIFeedbackGenerator and its
  // concrete subclasses are available on iOS 10+, they are not really
  // available on an app whose deployment target is iOS 10.0 (iOS 10.1+ are
  // okay) and Chrome will fail at dynamic link time and instantly crash.
  // NSClassFromString() checks if Objective-C run-time has the class before
  // using it.
  Class generatorClass = NSClassFromString(@"UINotificationFeedbackGenerator");
  if (generatorClass) {
    UINotificationFeedbackGenerator* generator = [[generatorClass alloc] init];
    [generator notificationOccurred:type];
  }
}

NSAttributedString* TextForTabCount(int count, CGFloat font_size) {
  NSString* string;
  if (count <= 0) {
    string = IsChromeNextIaEnabled() ? @"0" : @"";
  } else if (count > 99) {
    string = @":)";
  } else {
    string = [NSString stringWithFormat:@"%d", count];
  }

  UIFontWeight weight =
      UIAccessibilityIsBoldTextEnabled() ? UIFontWeightHeavy : UIFontWeightBold;
  UIFont* font = [UIFont systemFontOfSize:font_size weight:weight];
  UIFontDescriptor* descriptor = [font.fontDescriptor
      fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
  font = [UIFont fontWithDescriptor:descriptor size:font_size];

  return [[NSAttributedString alloc] initWithString:string
                                         attributes:@{
                                           NSFontAttributeName : font,
                                           NSKernAttributeName : @(-0.8),
                                         }];
}

NSAttributedString* TextForTabGroupCount(int count, CGFloat font_size) {
  NSString* string;
  if (count <= 0) {
    string = @"";
  } else if (count < 100) {
    string = [NSString stringWithFormat:@"%d", count];
  } else {
    string = @"99+";
  }

  UIFont* font = [UIFont systemFontOfSize:font_size weight:UIFontWeightMedium];
  UIFontDescriptor* descriptor = [font.fontDescriptor
      fontDescriptorWithDesign:UIFontDescriptorSystemDesignRounded];
  font = [UIFont fontWithDescriptor:descriptor size:font_size];

  return
      [[NSAttributedString alloc] initWithString:string
                                      attributes:@{NSFontAttributeName : font}];
}

UIView* ViewHierarchyRootForView(UIView* view) {
  if (view.window) {
    return view.window;
  }

  if (!view.superview) {
    return view;
  }

  return ViewHierarchyRootForView(view.superview);
}

CGFloat RemainingScrollDistanceToTop(UIScrollView* scroll_view) {
  return scroll_view.contentOffset.y + scroll_view.adjustedContentInset.top;
}

CGFloat RemainingScrollDistanceToBottom(UIScrollView* scroll_view) {
  CGFloat scrollable_height = scroll_view.contentSize.height +
                              scroll_view.adjustedContentInset.bottom -
                              scroll_view.bounds.size.height;
  return scrollable_height - scroll_view.contentOffset.y;
}

CGFloat DeviceCornerRadius() {
  UIWindow* window = nil;
  for (UIScene* scene in UIApplication.sharedApplication.connectedScenes) {
    UIWindowScene* windowScene =
        base::apple::ObjCCastStrict<UIWindowScene>(scene);
    UIWindow* firstWindow = [windowScene.windows firstObject];
    if (firstWindow) {
      window = firstWindow;
      break;
    }
  }

  // Estimated iPhone rounded corners radii.
  if (window.safeAreaInsets.bottom &&
      ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_PHONE) {
    return 50.0;
  }

  // Estimated iPad rounded corners radii.
  if (window.safeAreaInsets.bottom &&
      ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    return 18.0;
  }

  // Device has square corners.
  return 0.0;
}

bool IsBottomOmniboxAvailable() {
  return ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_PHONE;
}

NSArray<UITrait>* TraitCollectionSetForTraits(NSArray<UITrait>* traits) {
  if (base::FeatureList::IsEnabled(kEnableTraitCollectionRegistration) &&
      traits) {
    return traits;
  }

  static dispatch_once_t once;
  static NSArray<UITrait>* everyUIMutableTrait = nil;
  dispatch_once(&once, ^{
    // This is a list of all the UITraits provided by iOS. This was generated
    // from Apple's documentation on UIMutableTraits and is subject to change
    // with subsequent releases of iOS. See
    // https://developer.apple.com/documentation/uikit/uimutabletraits?language=objc
    NSMutableArray<UITrait>* mutableTraits = [@[
      UITraitAccessibilityContrast.class, UITraitActiveAppearance.class,
      UITraitDisplayGamut.class, UITraitDisplayScale.class,
      UITraitForceTouchCapability.class, UITraitHorizontalSizeClass.class,
      UITraitImageDynamicRange.class, UITraitLayoutDirection.class,
      UITraitLegibilityWeight.class, UITraitPreferredContentSizeCategory.class,
      UITraitSceneCaptureState.class, UITraitToolbarItemPresentationSize.class,
      UITraitTypesettingLanguage.class, UITraitUserInterfaceIdiom.class,
      UITraitUserInterfaceLevel.class, UITraitUserInterfaceStyle.class,
      UITraitVerticalSizeClass.class, UITraitListEnvironment.class
    ] mutableCopy];

    everyUIMutableTrait = [NSArray arrayWithArray:mutableTraits];
  });

  return everyUIMutableTrait;
}

size_t MemoryFootprintForImage(UIImage* image) {
  CGImageRef cgImage = [image CGImage];
  size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
  size_t height = CGImageGetHeight(cgImage);
  size_t totalBytes = bytesPerRow * height;
  return totalBytes / 1024;
}

void ExecuteWhenTransitionsComplete(ProceduralBlock action,
                                    UIViewController* viewController) {
  if (!viewController) {
    return;
  }

  // If there is another transition going on (e.g. dismissal/presentation),
  // postpone the execution of the block to allow it to complete first.
  id<UIViewControllerTransitionCoordinator> transitionCoordinator =
      viewController.transitionCoordinator;
  if (transitionCoordinator) {
    __weak UIViewController* weakViewController = viewController;
    [transitionCoordinator
        animateAlongsideTransition:nil
                        completion:^(
                            id<UIViewControllerTransitionCoordinatorContext>
                                context) {
                          // Re-evaluate in the next run loop turn.
                          web::GetUIThreadTaskRunner({})->PostTask(
                              FROM_HERE, base::BindOnce(^{
                                ExecuteWhenTransitionsComplete(
                                    action, weakViewController);
                              }));
                        }];
    return;
  }

  id<ContextMenuTransitionStateProviding> provider = nil;
  if ([viewController
          conformsToProtocol:@protocol(ContextMenuTransitionStateProviding)]) {
    provider = (id<ContextMenuTransitionStateProviding>)viewController;
  }

  if (provider.activeContextMenuAnimator) {
    __weak UIViewController* weakViewController = viewController;
    [provider.activeContextMenuAnimator addCompletion:^{
      // Re-evaluate in the next run loop turn.
      web::GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE, base::BindOnce(^{
            ExecuteWhenTransitionsComplete(action, weakViewController);
          }));
    }];
  } else {
    action();
  }
}
