// Copyright 2013 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/safe_mode/ui_bundled/safe_mode_view_controller.h"

#import <QuartzCore/QuartzCore.h>

#import <string_view>

#import "base/containers/fixed_flat_set.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/system/sys_info.h"
#import "ios/chrome/browser/crash_report/model/crash_helper.h"
#import "ios/chrome/browser/safe_mode/model/safe_mode_crashing_modules_config.h"
#import "ios/chrome/browser/safe_mode/model/safe_mode_util.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/common/crash_report/crash_helper.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ui/base/device_form_factor.h"

namespace {
const CGFloat kButtonCornerRadius = 15;
const CGFloat kVerticalSpacing = 20;
const CGFloat kUploadProgressSpacing = 5;
const NSTimeInterval kUploadPumpInterval = 0.1;
const NSTimeInterval kUploadTotalTime = 5;
constexpr std::string_view kThirdPartyModsDirectory =
    "/Library/MobileSubstrate/DynamicLibraries/";
}  // anonymous namespace

@interface SafeModeViewController ()
// Returns `YES` if any third-party modifications are detected.
+ (BOOL)detectedThirdPartyMods;
// Returns `YES` if the current OS version has known stability issues
// and is blocklisted.
+ (BOOL)isKnownBadOSVersion;
// Returns `YES` if there are crash reports to upload.
+ (BOOL)hasReportToUpload;
// Returns a message explaining which, if any, 3rd party modules were detected
// that may cause Chrome to crash.
- (NSString*)startupCrashModuleText;
// Starts timer to update progress bar for crash report upload.
- (void)startUploadProgress;
// Updates progress bar for crash report upload.
- (void)pumpUploadProgress;
// Returns `YES` if this VC should display the button to resume chrome.
- (BOOL)shouldShowStartButton;
// Called when user taps on "Resume Chrome" button. Notifies the delegate to
// attempt to start the browser.
- (void)startBrowserFromSafeMode;
@end

@implementation SafeModeViewController {
  __weak id<SafeModeViewControllerDelegate> _delegate;
  UIView* _innerView;
  UIButton* _startButton;
  UIView* _uploadDescriptionAnchorView;
  UILabel* _uploadDescription;
  UIProgressView* _uploadProgress;
  NSDate* _uploadStartTime;
  NSTimer* _uploadTimer;
}

- (id)initWithDelegate:(id<SafeModeViewControllerDelegate>)delegate {
  self = [super init];
  if (self) {
    _delegate = delegate;
  }
  return self;
}

+ (BOOL)hasSuggestions {
  if ([SafeModeViewController detectedThirdPartyMods]) {
    return YES;
  }

  static dispatch_once_t once_token = 0;
  dispatch_once(&once_token, ^{
    crash_helper::ProcessIntermediateReportsForSafeMode();
  });

  if ([SafeModeViewController isKnownBadOSVersion]) {
    return YES;
  }

  return [SafeModeViewController hasReportToUpload];
}

+ (BOOL)isKnownBadOSVersion {
  static constexpr auto kBadOSVersions =
      base::MakeFixedFlatSet<std::string_view>({
          "23a5260n",  // iOS 26 beta1
          "23a5260u",  // iOS 26 beta1 (rerelease)
          "23a5276f",  // iOS 26 beta2
          "23a5287g",  // iOS 26 beta3
      });

  std::string build = base::ToLowerASCII(base::SysInfo::GetIOSBuildNumber());
  return kBadOSVersions.contains(std::string_view(build));
}

+ (BOOL)detectedThirdPartyMods {
  std::vector<std::string> thirdPartyMods =
      safe_mode_util::GetLoadedImages(kThirdPartyModsDirectory);
  return (thirdPartyMods.size() > 0);
}

+ (BOOL)hasReportToUpload {
  // If uploading is enabled and more than one report has stacked up, then we
  // assume that the app may be in a state that is preventing crash report
  // uploads before crashing again.
  return crash_helper::common::UserEnabledUploading() &&
         crash_helper::GetPendingCrashReportCount() > 1;
}

// Return any jailbroken library that appears in SafeModeCrashingModulesConfig.
- (NSArray*)startupCrashModules {
  std::vector<std::string> modules =
      safe_mode_util::GetLoadedImages(kThirdPartyModsDirectory);
  NSMutableArray* array = [NSMutableArray arrayWithCapacity:modules.size()];
  SafeModeCrashingModulesConfig* config =
      [SafeModeCrashingModulesConfig sharedInstance];
  for (const std::string& module_name : modules) {
    NSString* path = base::SysUTF8ToNSString(module_name);
    NSString* friendlyName = [config startupCrashModuleFriendlyName:path];
    if (friendlyName != nil) {
      [array addObject:friendlyName];
    }
  }
  return array;
}

// Since we are still supporting iOS5, this is a helper for basic flow layout.
- (void)centerView:(UIView*)view afterView:(UIView*)afterView {
  CGPoint center = [view center];
  center.x = [_innerView frame].size.width / 2;
  [view setCenter:center];

  if (afterView) {
    CGRect frame = view.frame;
    frame.origin.y = CGRectGetMaxY(afterView.frame) + kVerticalSpacing;
    view.frame = frame;
  }
}

- (NSString*)startupCrashModuleText {
  NSArray* knownModules = [self startupCrashModules];
  if ([knownModules count]) {
    NSString* wrongText =
        NSLocalizedString(@"IDS_IOS_SAFE_MODE_NAMED_TWEAKS_FOUND", @"");
    NSMutableString* text = [NSMutableString stringWithString:wrongText];
    [text appendString:@"\n"];
    for (NSString* module in knownModules) {
      [text appendFormat:@"\n    %@", module];
    }
    return text;
  } else if ([SafeModeViewController detectedThirdPartyMods]) {
    return NSLocalizedString(@"IDS_IOS_SAFE_MODE_TWEAKS_FOUND", @"");
  } else if ([SafeModeViewController isKnownBadOSVersion]) {
    return NSLocalizedString(@"IDS_IOS_SAFE_MODE_NEEDS_OS_UPDATE", @"");
  } else {
    return NSLocalizedString(@"IDS_IOS_SAFE_MODE_UNKNOWN_CAUSE", @"");
  }
}

- (void)viewDidLoad {
  [super viewDidLoad];

  // Width of the inner view on iPhone.
  const CGFloat kIPhoneWidth = 250;
  // Width of the inner view on iPad.
  const CGFloat kIPadWidth = 350;
  // Horizontal buffer.
  const CGFloat kHorizontalSpacing = 20;

  self.view.autoresizesSubviews = YES;
  CGRect mainBounds =
      self.view.window ? self.view.window.bounds : self.view.bounds;
  // SafeModeViewController only supports portrait orientation (see
  // implementation of supportedInterfaceOrientations: below) but if the app is
  // launched from landscape mode (e.g. iPad or iPhone 6+) then the mainScreen's
  // bounds will still be landscape at this point. Swap the height and width
  // here so that the dimensions will be correct once the app rotates to
  // portrait.
  if (IsLandscape(self.view.window)) {
    mainBounds.size = CGSizeMake(mainBounds.size.height, mainBounds.size.width);
  }
  UIScrollView* scrollView = [[UIScrollView alloc] initWithFrame:mainBounds];
  self.view = scrollView;
  self.view.backgroundColor = [UIColor colorNamed:kBackgroundColor];
  const CGFloat kIPadInset =
      (mainBounds.size.width - kIPadWidth - kHorizontalSpacing) / 2;
  const CGFloat widthInset =
      (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET)
          ? kIPadInset
          : kHorizontalSpacing;
  _innerView = [[UIView alloc]
      initWithFrame:CGRectInset(mainBounds, widthInset, kVerticalSpacing * 2)];
  [scrollView addSubview:_innerView];

  UIImage* fatalImage = [[UIImage imageNamed:@"fatal_error.png"]
      imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
  UIImageView* imageView = [[UIImageView alloc] initWithImage:fatalImage];
  imageView.tintColor = [UIColor colorNamed:kPlaceholderImageTintColor];
  // Shift the image down a bit.
  CGRect imageFrame = [imageView frame];
  imageFrame.origin.y = kVerticalSpacing;
  [imageView setFrame:imageFrame];
  [self centerView:imageView afterView:nil];
  [_innerView addSubview:imageView];

  UILabel* awSnap = [[UILabel alloc] init];
  [awSnap setText:NSLocalizedString(@"IDS_IOS_SAFE_MODE_AW_SNAP", @"")];
  awSnap.textColor = [UIColor colorNamed:kTextPrimaryColor];
  [awSnap setFont:[UIFont boldSystemFontOfSize:21]];
  [awSnap sizeToFit];
  [self centerView:awSnap afterView:imageView];
  [_innerView addSubview:awSnap];

  UILabel* description = [[UILabel alloc] init];
  [description setText:[self startupCrashModuleText]];
  description.textColor = [UIColor colorNamed:kTextSecondaryColor];
  [description setTextAlignment:NSTextAlignmentCenter];
  [description setNumberOfLines:0];
  [description setLineBreakMode:NSLineBreakByWordWrapping];
  const CGFloat width =
      (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET)
          ? kIPadWidth
          : kIPhoneWidth;
  const CGSize size = [description sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)];
  CGRect frame = [description frame];
  frame.size = size;
  [description setFrame:frame];
  [self centerView:description afterView:awSnap];
  [_innerView addSubview:description];

  UIView* lastView = description;
  if ([self shouldShowStartButton]) {
    _startButton = [UIButton buttonWithType:UIButtonTypeSystem];

    UIButtonConfiguration* buttonConfiguration =
        [UIButtonConfiguration plainButtonConfiguration];
    NSString* startText =
        NSLocalizedString(@"IDS_IOS_SAFE_MODE_RELOAD_CHROME", @"");
    buttonConfiguration.title = startText;
    buttonConfiguration.background.backgroundColor =
        [UIColor colorNamed:kBlueColor];
    buttonConfiguration.background.cornerRadius = kButtonCornerRadius;
    buttonConfiguration.titleAlignment =
        UIButtonConfigurationTitleAlignmentCenter;
    buttonConfiguration.baseForegroundColor =
        [UIColor colorNamed:kSolidWhiteColor];
    buttonConfiguration.titleLineBreakMode = NSLineBreakByWordWrapping;
    _startButton.configuration = buttonConfiguration;
    _startButton.configurationUpdateHandler = ^(UIButton* button) {
      UIButtonConfiguration* innerConfiguration = button.configuration;
      innerConfiguration.background.backgroundColor =
          button.enabled ? [UIColor colorNamed:kBlueColor]
                         : [UIColor colorNamed:kGrey300Color];
      button.configuration = innerConfiguration;
    };

    frame = [_startButton frame];
    frame.size.width =
        (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET)
            ? kIPadWidth
            : kIPhoneWidth;
    frame.size.height = _startButton.intrinsicContentSize.height;
    [_startButton setFrame:frame];
    [_startButton addTarget:self
                     action:@selector(startBrowserFromSafeMode)
           forControlEvents:UIControlEventTouchUpInside];
    [self centerView:_startButton afterView:description];
    [_innerView addSubview:_startButton];

    lastView = _startButton;
  }

  if ([SafeModeViewController hasReportToUpload]) {
    crash_helper::StartUploadingReportsInRecoveryMode();

    // If there are no jailbreak modifications, then present the "Sending crash
    // report..." UI.
    if (![SafeModeViewController detectedThirdPartyMods]) {
      [_startButton setEnabled:NO];

      _uploadDescriptionAnchorView = lastView;
      _uploadDescription = [[UILabel alloc] init];
      [_uploadDescription
          setText:NSLocalizedString(@"IDS_IOS_SAFE_MODE_SENDING_CRASH_REPORT",
                                    @"")];
      [_uploadDescription setFont:[UIFont systemFontOfSize:13]];
      _uploadDescription.textColor = [UIColor colorNamed:kTextSecondaryColor];
      [_uploadDescription sizeToFit];
      [self centerView:_uploadDescription afterView:lastView];
      [_innerView addSubview:_uploadDescription];

      _uploadProgress = [[UIProgressView alloc]
          initWithProgressViewStyle:UIProgressViewStyleDefault];
      [self centerView:_uploadProgress afterView:nil];
      frame = [_uploadProgress frame];
      frame.origin.y =
          CGRectGetMaxY([_uploadDescription frame]) + kUploadProgressSpacing;
      [_uploadProgress setFrame:frame];
      [_innerView addSubview:_uploadProgress];

      lastView = _uploadProgress;
      [self startUploadProgress];
    }
  }

  CGSize scrollSize =
      CGSizeMake(mainBounds.size.width,
                 CGRectGetMaxY([lastView frame]) + kVerticalSpacing);
  frame = [_innerView frame];
  frame.size.height = scrollSize.height;
  [_innerView setFrame:frame];
  scrollSize.height += frame.origin.y;
  [scrollView setContentSize:scrollSize];
}

- (NSUInteger)supportedInterfaceOrientations {
  return UIInterfaceOrientationMaskPortrait;
}

#pragma mark - Private

- (void)startUploadProgress {
  _uploadStartTime = [NSDate date];
  _uploadTimer =
      [NSTimer scheduledTimerWithTimeInterval:kUploadPumpInterval
                                       target:self
                                     selector:@selector(pumpUploadProgress)
                                     userInfo:nil
                                      repeats:YES];
}

- (void)pumpUploadProgress {
  NSTimeInterval elapsed =
      [[NSDate date] timeIntervalSinceDate:_uploadStartTime];
  // Theoretically we could stop early when the value returned by
  // crash_helper::GetCrashReportCount() changes, but this is simpler. If we
  // decide to look for a change in crash report count, then we also probably
  // want to replace the UIProgressView with a UIActivityIndicatorView.
  if (elapsed <= kUploadTotalTime) {
    [_uploadProgress setProgress:elapsed / kUploadTotalTime animated:YES];
  } else {
    [_uploadTimer invalidate];

    [_startButton setEnabled:YES];
    [_uploadDescription
        setText:NSLocalizedString(@"IDS_IOS_SAFE_MODE_CRASH_REPORT_SENT", @"")];
    [_uploadDescription sizeToFit];
    [self centerView:_uploadDescription afterView:_uploadDescriptionAnchorView];
    [_uploadProgress setHidden:YES];
  }
}

- (BOOL)shouldShowStartButton {
  return ![SafeModeViewController isKnownBadOSVersion];
}

- (void)startBrowserFromSafeMode {
  [_delegate startBrowserFromSafeMode];
}

@end
