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

#import <WebKit/WebKit.h>

#import <memory>
#import <unordered_map>
#import <utility>

#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/json/json_writer.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "components/autofill/ios/browser/autofill_agent.h"
#import "components/password_manager/core/browser/password_manager.h"
#import "components/password_manager/ios/password_controller_driver_helper.h"
#import "components/password_manager/ios/shared_password_controller.h"
#import "components/safe_browsing/ios/browser/safe_browsing_url_allow_list.h"
#import "components/url_formatter/elide_url.h"
#import "ios/components/security_interstitials/lookalikes/lookalike_url_container.h"
#import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_allow_list.h"
#import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.h"
#import "ios/components/security_interstitials/safe_browsing/safe_browsing_client.h"
#import "ios/components/security_interstitials/safe_browsing/safe_browsing_query_manager.h"
#import "ios/components/security_interstitials/safe_browsing/safe_browsing_tab_helper.h"
#import "ios/components/security_interstitials/safe_browsing/safe_browsing_unsafe_resource_container.h"
#import "ios/web/navigation/nscoder_util.h"
#import "ios/web/public/favicon/favicon_url.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/referrer.h"
#import "ios/web/public/navigation/reload_type.h"
#import "ios/web/public/session/proto/metadata.pb.h"
#import "ios/web/public/session/proto/storage.pb.h"
#import "ios/web/public/ui/context_menu_params.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_client.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_view_only/wk_web_view_configuration_util.h"
#import "ios/web_view/internal/app/application_context.h"
#import "ios/web_view/internal/autofill/cwv_autofill_controller_internal.h"
#import "ios/web_view/internal/autofill/web_view_autofill_client_ios.h"
#import "ios/web_view/internal/cwv_back_forward_list_internal.h"
#import "ios/web_view/internal/cwv_favicon_internal.h"
#import "ios/web_view/internal/cwv_find_in_page_controller_internal.h"
#import "ios/web_view/internal/cwv_global_state_internal.h"
#import "ios/web_view/internal/cwv_html_element_internal.h"
#import "ios/web_view/internal/cwv_navigation_action_internal.h"
#import "ios/web_view/internal/cwv_ssl_status_internal.h"
#import "ios/web_view/internal/cwv_web_view_configuration_internal.h"
#import "ios/web_view/internal/cwv_web_view_internal.h"
#import "ios/web_view/internal/language/web_view_url_language_histogram_factory.h"
#import "ios/web_view/internal/passwords/web_view_password_manager_client.h"
#import "ios/web_view/internal/safe_browsing/web_view_safe_browsing_client_factory.h"
#import "ios/web_view/internal/translate/cwv_translation_controller_internal.h"
#import "ios/web_view/internal/translate/web_view_translate_client.h"
#import "ios/web_view/internal/web_view_browser_state.h"
#import "ios/web_view/internal/web_view_java_script_dialog_presenter.h"
#import "ios/web_view/internal/web_view_message_handler_java_script_feature.h"
#import "ios/web_view/internal/web_view_web_state_policy_decider.h"
#import "ios/web_view/public/cwv_navigation_delegate.h"
#import "ios/web_view/public/cwv_preferences.h"
#import "ios/web_view/public/cwv_preview_element_info.h"
#import "ios/web_view/public/cwv_ui_delegate.h"
#import "ios/web_view/public/cwv_web_view_configuration.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/page_transition_types.h"
#import "url/gurl.h"

namespace {

BOOL gChromeContextMenuEnabled = NO;
BOOL gWebInspectorEnabled = NO;
BOOL gSkipAccountStorageCheckEnabled = NO;

// Keys used to store CWVWebViewProtobufStorage and its properties.
NSString* const kProtobufStorageKey = @"protobufStorage";
NSString* const kStorageKey = @"storage";
NSString* const kSessionKey = @"session";

// Policy to use when realizing the deserialized WebState.
constexpr web::WebState::RealizationPolicy kRealizationPolicy =
    web::WebState::RealizationPolicy::kEnforceNoAttachedData;

// Converts base::Value expected to be a dictionary or list to NSDictionary or
// NSArray, respectively.
id NSObjectFromCollectionValue(const base::Value* value) {
  DCHECK(value->is_dict() || value->is_list())
      << "Incorrect value type: " << value->type();

  std::string json;
  const bool success = base::JSONWriter::Write(*value, &json);
  DCHECK(success) << "Failed to convert base::Value to JSON";

  NSData* json_data = [NSData dataWithBytes:json.c_str() length:json.length()];
  id ns_object = [NSJSONSerialization JSONObjectWithData:json_data
                                                 options:kNilOptions
                                                   error:nil];
  DCHECK(ns_object) << "Failed to convert JSON to Collection";
  return ns_object;
}

// Converts base::Value to an appropriate Obj-C object.
// |value| must not be null.
id NSObjectFromValue(const base::Value* value) {
  switch (value->type()) {
    case base::Value::Type::NONE:
      return nil;
    case base::Value::Type::BOOLEAN:
      return @(value->GetBool());
    case base::Value::Type::INTEGER:
      return @(value->GetInt());
    case base::Value::Type::DOUBLE:
      return @(value->GetDouble());
    case base::Value::Type::STRING:
      return base::SysUTF8ToNSString(value->GetString());
    case base::Value::Type::BINARY:
      // Unsupported.
      return nil;
    case base::Value::Type::DICT:
    case base::Value::Type::LIST:
      return NSObjectFromCollectionValue(value);
  }
  return nil;
}

// Converts base::DictValue to NSDictionary.
NSDictionary* NSDictionaryFromDictValue(const base::DictValue& value) {
  std::string json;
  const bool success = base::JSONWriter::Write(value, &json);
  DCHECK(success) << "Failed to convert base::Value to JSON";

  NSData* json_data = [NSData dataWithBytes:json.c_str() length:json.length()];
  NSDictionary* ns_dictionary = base::apple::ObjCCastStrict<NSDictionary>(
      [NSJSONSerialization JSONObjectWithData:json_data
                                      options:kNilOptions
                                        error:nil]);
  DCHECK(ns_dictionary) << "Failed to convert JSON to NSDictionary";
  return ns_dictionary;
}

// A WebStateUserData to hold a reference to a corresponding CWVWebView.
class WebViewHolder : public web::WebStateUserData<WebViewHolder> {
 public:
  explicit WebViewHolder(web::WebState* web_state) {}
  CWVWebView* web_view() const { return web_view_; }
  void set_web_view(CWVWebView* web_view) { web_view_ = web_view; }

 private:
  friend class web::WebStateUserData<WebViewHolder>;

  __weak CWVWebView* web_view_ = nil;
};

}  // namespace

// Used to serialize the protobuf message and the WebStateID.
@interface CWVWebViewProtobufStorage : NSObject <NSSecureCoding>

- (instancetype)initWithProto:(web::proto::WebStateStorage)storage
                   webStateID:(web::WebStateID)webStateID
    NS_DESIGNATED_INITIALIZER;

- (instancetype)initWithCoder:(NSCoder*)coder;

- (instancetype)init NS_UNAVAILABLE;

// Creates a new WebState with `browserState`.
- (std::unique_ptr<web::WebState>)createWebState:
    (web::BrowserState*)browserState;

@end

@implementation CWVWebViewProtobufStorage {
  web::proto::WebStateStorage _storage;
  web::WebStateID _webStateID;
}

- (instancetype)initWithProto:(web::proto::WebStateStorage)storage
                   webStateID:(web::WebStateID)webStateID {
  if ((self = [super init])) {
    _storage = std::move(storage);
    _webStateID = webStateID;
  }
  return self;
}

- (instancetype)initWithCoder:(NSCoder*)coder {
  if (![coder containsValueForKey:kStorageKey]) {
    return nil;
  }

  if (![coder containsValueForKey:kSessionKey]) {
    return nil;
  }

  const std::string buffer =
      web::nscoder_util::DecodeString(coder, kStorageKey);

  web::proto::WebStateStorage storage;
  if (!storage.ParseFromString(buffer)) {
    return nil;
  }

  web::WebStateID webStateID = web::WebStateID::FromSerializedValue(
      [coder decodeInt32ForKey:kSessionKey]);
  if (!webStateID.valid()) {
    return nil;
  }

  return [self initWithProto:std::move(storage) webStateID:webStateID];
}

- (void)encodeWithCoder:(NSCoder*)coder {
  std::string buffer;
  _storage.SerializeToString(&buffer);

  web::nscoder_util::EncodeString(coder, kStorageKey, buffer);
  [coder encodeInt32:_webStateID.identifier() forKey:kSessionKey];
}

- (std::unique_ptr<web::WebState>)createWebState:
    (web::BrowserState*)browserState {
  auto webState = web::WebState::CreateWithStorage(
      browserState, _webStateID, _storage.metadata(),
      base::ReturnValueOnce(std::make_optional(std::move(_storage))),
      base::ReturnValueOnce<NSData*>(nil));
  webState->ForceRealizedWithPolicy(kRealizationPolicy);
  return webState;
}

- (const web::proto::WebStateStorage&)storage {
  return _storage;
}

+ (BOOL)supportsSecureCoding {
  return YES;
}

@end

// Helper used to manage the serialization of CWVWebView's WebState.
@interface CWVWebViewSerializationHelper : NSObject

// Designated initializer.
- (instancetype)initWithConfiguration:(CWVWebViewConfiguration*)configuration
    NS_DESIGNATED_INITIALIZER;

- (instancetype)init NS_UNAVAILABLE;

// Creates a new WebState from `coder`.
- (std::unique_ptr<web::WebState>)createWebStateWithCoder:(NSCoder*)coder;

// Serializes `webState` to `coder`. If `webState` is null, then the cached
// state is serialized instead (captured by `-updateStateFromWebState:`).
- (void)encodeWebState:(web::WebState*)webState toCoder:(NSCoder*)coder;

// Updates the cached serialization state from `webState`.
- (void)updateStateFromWebState:(web::WebState*)webState;

// Clears the cached serialized state for `webState` if it is possible to
// reconstruct it at a later point (avoid keeping a copy of the state in
// memory when not necessary after serialization).
- (void)clearStateForWebStateIfPossible:(web::WebState*)webState;

@end

@implementation CWVWebViewSerializationHelper {
  web::BrowserState* _browserState;

  // Cached protobuf message.
  CWVWebViewProtobufStorage* _cachedProtobufStorage;
}

- (instancetype)initWithConfiguration:(CWVWebViewConfiguration*)configuration {
  if ((self = [super init])) {
    DCHECK(configuration.browserState);
    _browserState = configuration.browserState;
    if (configuration.preferences.isTriggerNonFatalCheckEnabled) {
      // TODO(crbug.com/503005390): Remove after release integration testing.
      CHECK(false, base::NotFatalUntil::M235);
    }
  }
  return self;
}

- (std::unique_ptr<web::WebState>)createWebStateWithCoder:(NSCoder*)coder {
  _cachedProtobufStorage =
      base::apple::ObjCCastStrict<CWVWebViewProtobufStorage>([coder
          decodeObjectOfClass:[CWVWebViewProtobufStorage class]
                       forKey:kProtobufStorageKey]);

  // If data can't be loaded, return a brand new WebState. Since sending
  // a message to nil is well-defined in Objective-C, this also cover
  // the case when `coder` is nil.
  if (!_cachedProtobufStorage) {
    const web::WebState::CreateParams createParams(_browserState);
    return web::WebState::Create(createParams);
  }

  return [_cachedProtobufStorage createWebState:_browserState];
}

- (void)encodeWebState:(web::WebState*)webState toCoder:(NSCoder*)coder {
  if (webState && webState->IsRealized()) {
    [self updateStateFromWebState:webState];
  }

  [coder encodeObject:_cachedProtobufStorage forKey:kProtobufStorageKey];
  [self clearStateForWebStateIfPossible:webState];
}

- (void)updateStateFromWebState:(web::WebState*)webState {
  DCHECK(webState->IsRealized());
  web::proto::WebStateStorage storage;
  webState->SerializeToProto(storage);
  _cachedProtobufStorage = [[CWVWebViewProtobufStorage alloc]
      initWithProto:storage
         webStateID:webState->GetUniqueIdentifier()];
}

- (void)clearStateForWebStateIfPossible:(web::WebState*)webState {
  if (webState && webState->IsRealized()) {
    _cachedProtobufStorage = nil;
  }
}

@end

@interface CWVWebView () {
  CWVWebViewConfiguration* _configuration;
  std::unique_ptr<web::WebState> _webState;
  std::unique_ptr<web::WebStateDelegateBridge> _webStateDelegate;
  std::unique_ptr<web::WebStateObserverBridge> _webStateObserver;
  std::unique_ptr<ios_web_view::WebViewWebStatePolicyDecider>
      _webStatePolicyDecider;
  double _estimatedProgress;
  // Handles presentation of JavaScript dialogs.
  std::unique_ptr<ios_web_view::WebViewJavaScriptDialogPresenter>
      _javaScriptDialogPresenter;

  // Handle the serialization and deserialization.
  CWVWebViewSerializationHelper* _serializationHelper;
}

// Redefine these properties as readwrite to define setters, which send KVO
// notifications.
@property(nonatomic, readwrite) double estimatedProgress;
@property(nonatomic, readwrite) BOOL canGoBack;
@property(nonatomic, readwrite) BOOL canGoForward;
@property(nonatomic, readwrite) NSURL* lastCommittedURL;
@property(nonatomic, readwrite) BOOL loading;
@property(nonatomic, readwrite, copy) NSString* title;
@property(nonatomic, readwrite) NSURL* visibleURL;
@property(nonatomic, readwrite) NSString* visibleLocationString;
@property(nonatomic, readwrite) CWVSSLStatus* visibleSSLStatus;

// Updates the availability of the back/forward navigation properties exposed
// through |canGoBack| and |canGoForward|, and also updates |backForwardList|.
- (void)updateNavigationAvailability;
// Updates the URLs exposed through |lastCommittedURL| and |visibleURL|.
- (void)updateCurrentURLs;
// Updates |title| property.
- (void)updateTitle;
// Returns a new CWVAutofillController created from |_webState|.
- (CWVAutofillController*)newAutofillController;
// Returns a new CWVTranslationController created from |_webState|.
- (CWVTranslationController*)newTranslationController;
// Updates |_webState| visiblity.
- (void)updateWebStateVisibility;

@end

@implementation CWVWebView

@synthesize autofillController = _autofillController;
@synthesize backForwardList = _backForwardList;
@synthesize canGoBack = _canGoBack;
@synthesize canGoForward = _canGoForward;
@synthesize configuration = _configuration;
@synthesize estimatedProgress = _estimatedProgress;
@synthesize findInPageController = _findInPageController;
@synthesize lastCommittedURL = _lastCommittedURL;
@synthesize loading = _loading;
@synthesize navigationDelegate = _navigationDelegate;
@synthesize title = _title;
@synthesize translationController = _translationController;
@synthesize UIDelegate = _UIDelegate;
@synthesize visibleURL = _visibleURL;
@synthesize visibleSSLStatus = _visibleSSLStatus;

+ (void)initialize {
  if (self != [CWVWebView class]) {
    return;
  }

  DCHECK([[CWVGlobalState sharedInstance] isStarted]);
  [[CWVGlobalState sharedInstance] start];
}

+ (BOOL)chromeContextMenuEnabled {
  return gChromeContextMenuEnabled;
}

+ (void)setChromeContextMenuEnabled:(BOOL)newValue {
  gChromeContextMenuEnabled = newValue;
}

+ (BOOL)useOptimizedSessionStorage {
  return YES;
}

+ (void)setUseOptimizedSessionStorage:(BOOL)newValue {
  // Ignored.
}

+ (BOOL)webInspectorEnabled {
  return gWebInspectorEnabled;
}

+ (void)setWebInspectorEnabled:(BOOL)newValue {
  gWebInspectorEnabled = newValue;
}

+ (BOOL)skipAccountStorageCheckEnabled {
  return gSkipAccountStorageCheckEnabled;
}

+ (void)setSkipAccountStorageCheckEnabled:(BOOL)newValue {
  gSkipAccountStorageCheckEnabled = newValue;
}

+ (CWVWebView*)webViewForWebState:(web::WebState*)webState {
  WebViewHolder* holder = WebViewHolder::FromWebState(webState);
  CWVWebView* webView = holder->web_view();
  DCHECK(webView);
  return webView;
}

- (instancetype)initWithFrame:(CGRect)frame
                configuration:(CWVWebViewConfiguration*)configuration {
  return [self initWithFrame:frame
               configuration:configuration
             WKConfiguration:nil
            createdWKWebView:nil];
}

- (instancetype)initWithFrame:(CGRect)frame
                configuration:(CWVWebViewConfiguration*)configuration
              WKConfiguration:(WKWebViewConfiguration*)wkConfiguration
             createdWKWebView:(WKWebView**)createdWebView {
  self = [super initWithFrame:frame];
  if (self) {
    _configuration = configuration;
    [_configuration registerWebView:self];

    _serializationHelper = [[CWVWebViewSerializationHelper alloc]
        initWithConfiguration:configuration];

    [self resetWebStateWithCoder:nil
                 WKConfiguration:wkConfiguration
                  createdWebView:createdWebView];
  }
  return self;
}

- (UIScrollView*)scrollView {
  if (![self isWebStateSafeToUse]) {
    return nil;
  }
  return [_webState->GetWebViewProxy().scrollViewProxy asUIScrollView];
}

- (BOOL)allowsBackForwardNavigationGestures {
  if (![self isWebStateSafeToUse]) {
    return NO;
  }

  return _webState->GetWebViewProxy().allowsBackForwardNavigationGestures;
}

- (void)setAllowsBackForwardNavigationGestures:
    (BOOL)allowsBackForwardNavigationGestures {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  _webState->GetWebViewProxy().allowsBackForwardNavigationGestures =
      allowsBackForwardNavigationGestures;
}

- (BOOL)allowsLinkPreview {
  if (![self isWebStateSafeToUse]) {
    return NO;
  }

  return _webState->GetWebViewProxy().allowsLinkPreview;
}

- (void)setAllowsLinkPreview:(BOOL)allowsLinkPreview {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  _webState->GetWebViewProxy().allowsLinkPreview = allowsLinkPreview;
}

- (void)goBack {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  if (_webState->GetNavigationManager()) {
    _webState->GetNavigationManager()->GoBack();
  }
}

- (void)goForward {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  if (_webState->GetNavigationManager()) {
    _webState->GetNavigationManager()->GoForward();
  }
}

- (BOOL)goToBackForwardListItem:(CWVBackForwardListItem*)item {
  if (![self isWebStateSafeToUse]) {
    return NO;
  }
  if (!_backForwardList) {
    return NO;  // Do nothing if |_backForwardList| is not generated yet.
  }

  if ([item isEqual:_backForwardList.currentItem]) {
    return NO;
  }

  int index = [_backForwardList internalIndexOfItem:item];
  if (index == -1) {
    return NO;
  }

  DCHECK(_webState);
  web::NavigationManager* navigationManager = _webState->GetNavigationManager();
  navigationManager->GoToIndex(index);
  return YES;
}

- (void)reload {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  // |check_for_repost| is false because CWVWebView does not support repost form
  // dialogs.
  _webState->GetNavigationManager()->Reload(web::ReloadType::NORMAL,
                                            false /* check_for_repost */);
}

- (void)stopLoading {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  _webState->Stop();
}

- (void)loadRequest:(NSURLRequest*)request {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  DCHECK_EQ(nil, request.HTTPBodyStream)
      << "request.HTTPBodyStream is not supported.";

  web::NavigationManager::WebLoadParams params(net::GURLWithNSURL(request.URL));
  params.transition_type = ui::PAGE_TRANSITION_TYPED;
  params.extra_headers = [request.allHTTPHeaderFields copy];
  params.post_data = [request.HTTPBody copy];
  _webState->GetNavigationManager()->LoadURLWithParams(params);
  [self updateCurrentURLs];
}

- (void)evaluateJavaScript:(NSString*)javaScriptString
         completionHandler:(void (^)(id result, NSError* error))completion {
  if (![self isWebStateSafeToUse]) {
    return;
  }

  web::WebFrame* mainFrame =
      _webState->GetPageWorldWebFramesManager()->GetMainWebFrame();
  if (!mainFrame) {
    if (completion) {
      completion(nil, [NSError errorWithDomain:@"org.chromium.chromewebview"
                                          code:0
                                      userInfo:nil]);
    }
    return;
  }

  if (!completion) {
    mainFrame->ExecuteJavaScript(base::SysNSStringToUTF16(javaScriptString));
    return;
  }

  mainFrame->ExecuteJavaScript(
      base::SysNSStringToUTF16(javaScriptString),
      base::BindOnce(^(const base::Value* result, NSError* error) {
        id jsResult = nil;
        if (!error && result) {
          jsResult = NSObjectFromValue(result);
        }
        completion(jsResult, error);
      }));
}

- (void)evaluateJavaScript:(NSString*)javaScriptString
                completion:(void (^)(id, NSError*))completion {
  [self evaluateJavaScript:javaScriptString completionHandler:completion];
}

- (void)setUIDelegate:(id<CWVUIDelegate>)UIDelegate {
  _UIDelegate = UIDelegate;

  _javaScriptDialogPresenter->SetUIDelegate(_UIDelegate);
}

- (void)setNavigationDelegate:(id<CWVNavigationDelegate>)navigationDelegate {
  _navigationDelegate = navigationDelegate;

  [self attachSecurityInterstitialHelpersToWebStateIfNecessary];
}

#pragma mark - UIResponder

- (BOOL)becomeFirstResponder {
  if ([self isWebStateSafeToUse]) {
    return [_webState->GetWebViewProxy() becomeFirstResponder];
  } else {
    return [super becomeFirstResponder];
  }
}

#pragma mark - UIView

- (void)didMoveToSuperview {
  [super didMoveToSuperview];

  [self updateWebStateVisibility];
}

#pragma mark - CRWWebStateObserver

- (void)webStateDestroyed:(web::WebState*)webState {
  webState->RemoveObserver(_webStateObserver.get());
  _webStateObserver.reset();
}

- (void)webState:(web::WebState*)webState
    didStartNavigation:(web::NavigationContext*)navigation {
  [self updateNavigationAvailability];

  if (!navigation->IsSameDocument()) {
    SEL oldSelector = @selector(webViewDidStartProvisionalNavigation:);
    if ([_navigationDelegate respondsToSelector:oldSelector]) {
      [_navigationDelegate webViewDidStartProvisionalNavigation:self];
    }
    SEL newSelector = @selector(webViewDidStartNavigation:);
    if ([_navigationDelegate respondsToSelector:newSelector]) {
      [_navigationDelegate webViewDidStartNavigation:self];
    }
  }
}

- (void)webState:(web::WebState*)webState
    didFinishNavigation:(web::NavigationContext*)navigation {
  [self updateNavigationAvailability];
  [self updateCurrentURLs];

  // TODO(crbug.com/41422373): Remove this once crbug.com/898357 is fixed.
  [self updateVisibleSSLStatus];

  if (navigation->HasCommitted() && !navigation->IsSameDocument() &&
      [_navigationDelegate
          respondsToSelector:@selector(webViewDidCommitNavigation:)]) {
    [_navigationDelegate webViewDidCommitNavigation:self];
  }

  NSError* error = navigation->GetError();
  SEL selector = @selector(webView:didFailNavigationWithError:);
  if (error && [_navigationDelegate respondsToSelector:selector]) {
    [_navigationDelegate webView:self didFailNavigationWithError:error];
  }
}

- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success {
  DCHECK_EQ(_webState.get(), webState);
  if (!success) {
    // Failure callbacks will be handled inside |webState:didFinishNavigation:|.
    return;
  }

  // TODO(crbug.com/40872106): Fragment navigations currently skip calling
  // `webViewDidStartNavigation:` and `webViewDidCommitNavigation:`, and instead
  // only calls `webViewDidFinishNavigation:` below. Fix this inconsistency.
  SEL selector = @selector(webViewDidFinishNavigation:);
  if ([_navigationDelegate respondsToSelector:selector]) {
    [_navigationDelegate webViewDidFinishNavigation:self];
  }
}

- (void)webState:(web::WebState*)webState
    didChangeLoadingProgress:(double)progress {
  self.estimatedProgress = progress;
}

- (void)webStateDidChangeBackForwardState:(web::WebState*)webState {
  [self updateNavigationAvailability];
}

- (void)webStateDidStopLoading:(web::WebState*)webState {
  self.loading = _webState->IsLoading();
}

- (void)webStateDidStartLoading:(web::WebState*)webState {
  self.loading = _webState->IsLoading();
}

- (void)webStateDidChangeTitle:(web::WebState*)webState {
  [self updateTitle];
}

- (void)webStateDidChangeVisibleSecurityState:(web::WebState*)webState {
  [self updateVisibleSSLStatus];
}

- (void)renderProcessGoneForWebState:(web::WebState*)webState {
  SEL selector = @selector(webViewWebContentProcessDidTerminate:);
  if ([_navigationDelegate respondsToSelector:selector]) {
    [_navigationDelegate webViewWebContentProcessDidTerminate:self];
  }
}

#pragma mark - CRWWebStateDelegate

- (web::WebState*)webState:(web::WebState*)webState
    createNewWebStateForURL:(const GURL&)URL
                  openerURL:(const GURL&)openerURL
            initiatedByUser:(BOOL)initiatedByUser {
  SEL selector = @selector(webView:
      createWebViewWithConfiguration:forNavigationAction:);
  if (![_UIDelegate respondsToSelector:selector]) {
    return nullptr;
  }

  NSURLRequest* request =
      [[NSURLRequest alloc] initWithURL:net::NSURLWithGURL(URL)];

  // The current implemention can't get the real navigation type for the
  // navigation action which causes a new web view be created. So uses
  // `CWVNavigationTypeNewWindow` before the real navigation type can be gotten
  // here.
  CWVNavigationAction* navigationAction =
      [[CWVNavigationAction alloc] initWithRequest:request
                                     userInitiated:initiatedByUser
                                    navigationType:CWVNavigationTypeNewWindow];
  CWVWebView* webView = [_UIDelegate webView:self
              createWebViewWithConfiguration:_configuration
                         forNavigationAction:navigationAction];
  if (webView) {
    web::WebState* webViewWebState = webView->_webState.get();
    if (webViewWebState) {
      webViewWebState->SetHasOpener(true);
      return webViewWebState;
    }
  }

  return nullptr;
}

- (void)closeWebState:(web::WebState*)webState {
  SEL selector = @selector(webViewDidClose:);
  if ([_UIDelegate respondsToSelector:selector]) {
    [_UIDelegate webViewDidClose:self];
  }
}

- (web::WebState*)webState:(web::WebState*)webState
         openURLWithParams:(const web::WebState::OpenURLParams&)params {
  web::NavigationManager::WebLoadParams load_params(params.url);
  load_params.referrer = params.referrer;
  load_params.transition_type = params.transition;
  load_params.is_renderer_initiated = params.is_renderer_initiated;
  load_params.virtual_url = params.virtual_url;
  _webState->GetNavigationManager()->LoadURLWithParams(load_params);
  [self updateCurrentURLs];
  return _webState.get();
}

- (web::JavaScriptDialogPresenter*)javaScriptDialogPresenterForWebState:
    (web::WebState*)webState {
  return _javaScriptDialogPresenter.get();
}

- (void)webState:(web::WebState*)webState
    handlePermissions:(NSArray<NSNumber*>*)permissions
      decisionHandler:(web::WebStatePermissionDecisionHandler)decisionHandler {
  DCHECK(decisionHandler);
  CWVMediaCaptureType mediaCaptureType;
  BOOL cameraPermissionRequested =
      [permissions containsObject:@(web::PermissionCamera)];
  BOOL micPermissionRequested =
      [permissions containsObject:@(web::PermissionMicrophone)];
  if (cameraPermissionRequested && micPermissionRequested) {
    mediaCaptureType = CWVMediaCaptureTypeCameraAndMicrophone;
  } else if (cameraPermissionRequested) {
    mediaCaptureType = CWVMediaCaptureTypeCamera;
  } else if (micPermissionRequested) {
    mediaCaptureType = CWVMediaCaptureTypeMicrophone;
  } else {
    NOTREACHED() << "Unknown media permissions";
  }

  SEL selector = @selector(webView:
      requestMediaCapturePermissionForType:decisionHandler:);
  if ([_UIDelegate respondsToSelector:selector]) {
    [_UIDelegate webView:self
        requestMediaCapturePermissionForType:mediaCaptureType
                             decisionHandler:^(CWVPermissionDecision decision) {
                               switch (decision) {
                                 case CWVPermissionDecisionPrompt:
                                   decisionHandler(
                                       web::
                                           PermissionDecisionShowDefaultPrompt);
                                   break;
                                 case CWVPermissionDecisionGrant:
                                   decisionHandler(
                                       web::PermissionDecisionGrant);
                                   break;
                                 case CWVPermissionDecisionDeny:
                                   decisionHandler(web::PermissionDecisionDeny);
                                   break;
                               }
                             }];
  } else {
    decisionHandler(web::PermissionDecisionShowDefaultPrompt);
  }
}

- (void)webState:(web::WebState*)webState
    contextMenuConfigurationForParams:(const web::ContextMenuParams&)params
                    completionHandler:(void (^)(UIContextMenuConfiguration*))
                                          completionHandler {
  SEL selector = @selector(webView:
      contextMenuConfigurationForElement:completionHandler:);
  if ([_UIDelegate respondsToSelector:selector]) {
    NSURL* hyperlink = net::NSURLWithGURL(params.link_url);
    NSURL* mediaSource = net::NSURLWithGURL(params.src_url);
    CWVHTMLElement* HTMLElement =
        [[CWVHTMLElement alloc] initWithHyperlink:hyperlink
                                      mediaSource:mediaSource
                                             text:params.text];

    [_UIDelegate webView:self
        contextMenuConfigurationForElement:HTMLElement
                         completionHandler:completionHandler];
  } else {
    completionHandler(nil);
  }
}

- (void)webState:(web::WebState*)webState
    contextMenuWillCommitWithAnimator:
        (id<UIContextMenuInteractionCommitAnimating>)animator {
  SEL selector = @selector(webView:contextMenuWillCommitWithAnimator:);
  if ([_UIDelegate respondsToSelector:selector]) {
    [_UIDelegate webView:self contextMenuWillCommitWithAnimator:animator];
  }
}

- (void)webState:(web::WebState*)webState
    didUpdateFaviconURLCandidates:
        (const std::vector<web::FaviconURL>&)candidates {
  if ([_UIDelegate respondsToSelector:@selector(webView:didLoadFavicons:)]) {
    [_UIDelegate webView:self
         didLoadFavicons:[CWVFavicon faviconsFromFaviconURLs:candidates]];
  }
}

- (id<CRWResponderInputView>)webStateInputViewProvider:
    (web::WebState*)webState {
  if (self.inputAccessoryView != nil) {
    return self;
  } else {
    return nil;
  }
}

- (void)addMessageHandler:(void (^)(NSDictionary* payload))handler
               forCommand:(NSString*)nsCommand {
  DCHECK(handler);
  std::string command = base::SysNSStringToUTF8(nsCommand);
  WebViewMessageHandlerJavaScriptFeature::GetInstance()->RegisterHandler(
      command, base::BindRepeating(^(const base::DictValue& payload) {
        handler(NSDictionaryFromDictValue(payload));
      }));
}

- (void)removeMessageHandlerForCommand:(NSString*)nsCommand {
  std::string command = base::SysNSStringToUTF8(nsCommand);
  WebViewMessageHandlerJavaScriptFeature::GetInstance()->UnregisterHandler(
      command);
}

#pragma mark - Translation

- (CWVTranslationController*)translationController {
  if (!_translationController) {
    _translationController = [self newTranslationController];
  }
  return _translationController;
}

- (CWVTranslationController*)newTranslationController {
  ios_web_view::WebViewBrowserState* browserState =
      ios_web_view::WebViewBrowserState::FromBrowserState(
          _webState->GetBrowserState());
  auto translateClient = ios_web_view::WebViewTranslateClient::Create(
      browserState, _webState.get());
  return [[CWVTranslationController alloc]
      initWithWebState:_webState.get()
       translateClient:std::move(translateClient)];
}

#pragma mark - Autofill

- (CWVAutofillController*)autofillController {
  if (!_autofillController) {
    _autofillController = [self newAutofillController];
  }
  return _autofillController;
}

- (CWVAutofillController*)newAutofillController {
  AutofillAgent* autofillAgent = [[AutofillAgent alloc]
      initWithPrefService:_configuration.browserState->GetPrefs()
                 webState:_webState.get()];

  auto passwordManagerClient =
      ios_web_view::WebViewPasswordManagerClient::Create(
          _webState.get(), _configuration.browserState);
  auto passwordManager = std::make_unique<password_manager::PasswordManager>(
      passwordManagerClient.get());

  PasswordFormHelper* formHelper =
      [[PasswordFormHelper alloc] initWithWebState:_webState.get()];
  PasswordSuggestionHelper* suggestionHelper =
      [[PasswordSuggestionHelper alloc] initWithWebState:_webState.get()
                                         passwordManager:passwordManager.get()];
  PasswordControllerDriverHelper* driverHelper =
      [[PasswordControllerDriverHelper alloc] initWithWebState:_webState.get()];
  SharedPasswordController* passwordController =
      [[SharedPasswordController alloc] initWithWebState:_webState.get()
                                                 manager:passwordManager.get()
                                              formHelper:formHelper
                                        suggestionHelper:suggestionHelper
                                            driverHelper:driverHelper];

  return [[CWVAutofillController alloc]
           initWithWebState:_webState.get()
              autofillAgent:autofillAgent
            passwordManager:std::move(passwordManager)
      passwordManagerClient:std::move(passwordManagerClient)
         passwordController:passwordController];
}

#pragma mark - Find In Page

- (CWVFindInPageController*)findInPageController {
  if (!_findInPageController) {
    _findInPageController =
        [[CWVFindInPageController alloc] initWithWebState:_webState.get()];
  }
  return _findInPageController;
}

#pragma mark - Preserving and Restoring State

- (void)encodeRestorableStateWithCoder:(NSCoder*)coder {
  [super encodeRestorableStateWithCoder:coder];
  [_serializationHelper encodeWebState:_webState.get() toCoder:coder];
}

- (void)decodeRestorableStateWithCoder:(NSCoder*)coder {
  [super decodeRestorableStateWithCoder:coder];
  [self resetWebStateWithCoder:coder WKConfiguration:nil createdWebView:nil];
}

#pragma mark - Private methods

- (void)updateWebStateVisibility {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  if (self.superview) {
    _webState->WasShown();
  } else {
    _webState->WasHidden();
  }
}

// Creates a WebState instance and assigns it to |_webState|.
// The WebState is restored from |coder| if available.
//
// If |wkConfiguration| is provided, the underlying WKWebView is
// initialized with |wkConfiguration|, and assigned to
// |*createdWKWebView| if |createdWKWebView| is not nil.
// |*createdWKWebView| will be provided only if |wkConfiguration| is provided,
// otherwise it will always be reset to nil.
- (void)resetWebStateWithCoder:(NSCoder*)coder
               WKConfiguration:(WKWebViewConfiguration*)wkConfiguration
                createdWebView:(WKWebView**)createdWebView {
  if (_webState) {
    if (_webStateObserver) {
      _webState->RemoveObserver(_webStateObserver.get());
    }
    WebViewHolder::RemoveFromWebState(_webState.get());
    if (_webState->GetView().superview == self) {
      // The web view provided by the old |_webState| has been added as a
      // subview. It must be removed and replaced with a new |_webState|'s web
      // view, which is added later.
      [_webState->GetView() removeFromSuperview];
    }
  }

  BOOL allowsBackForwardNavigationGestures =
      self.allowsBackForwardNavigationGestures;
  BOOL allowsLinkPreview = self.allowsLinkPreview;

  // CWVWebView does not support unrealized WebState, so ignore the
  // over-realization check (this simply reset the recent realization
  // counter, so do it each time a WebState is created).
  web::IgnoreOverRealizationCheck();

  _webState = [_serializationHelper createWebStateWithCoder:coder];
  DCHECK(_webState);

  // WARNING: NOTHING should be here between |web::WebState::Create()| and
  // |web::EnsureWebViewCreatedWithConfiguration()|, as this is the requirement
  // of |web::EnsureWebViewCreatedWithConfiguration()|

  WKWebView* webView = nil;
  if (wkConfiguration) {
    // When |wkConfiguration| is nil, |self| could be a newly opened web view
    // e.g., triggered by JavaScript "window.open()" function. In that case, if
    // |self| is not created by the WKWebViewConfiguration provided by WebKit's
    // delegate method
    // (https://cs.chromium.org/chromium/src/ios/web/web_state/ui/crw_wk_ui_handler.mm?q=crw_wk_ui_handler&sq=package:chromium&dr=C&l=61)
    // then calling |web::EnsureWebViewCreatedWithConfiguration()| here would
    // result in a crash (https://crbug.com/1054276). Now, we lazily create the
    // WKWebView inside |_webState| when |wkConfiguration| is not nil, and the
    // correct WKWebViewConfiguration will be passed inside //ios/web.
    webView = web::EnsureWebViewCreatedWithConfiguration(_webState.get(),
                                                         wkConfiguration);
  }

  if (createdWebView) {
    // If the created webView is needed, returns it by the out variable way.
    *createdWebView = webView;
  }

  [self attachSecurityInterstitialHelpersToWebStateIfNecessary];
  WebViewHolder::CreateForWebState(_webState.get());
  WebViewHolder::FromWebState(_webState.get())->set_web_view(self);

  if (!_webStateObserver) {
    _webStateObserver = std::make_unique<web::WebStateObserverBridge>(self);
  }
  _webState->AddObserver(_webStateObserver.get());

  if (_backForwardList) {
    _backForwardList.navigationManager = _webState->GetNavigationManager();
  }
  [self updateWebStateVisibility];

  _webStateDelegate = std::make_unique<web::WebStateDelegateBridge>(self);
  _webState->SetDelegate(_webStateDelegate.get());

  _webStatePolicyDecider =
      std::make_unique<ios_web_view::WebViewWebStatePolicyDecider>(
          _webState.get(), self);

  _javaScriptDialogPresenter =
      std::make_unique<ios_web_view::WebViewJavaScriptDialogPresenter>(self,
                                                                       nullptr);

  _webState->GetWebViewProxy().allowsBackForwardNavigationGestures =
      allowsBackForwardNavigationGestures;
  _webState->GetWebViewProxy().allowsLinkPreview = allowsLinkPreview;

  if (_translationController) {
    id<CWVTranslationControllerDelegate> delegate =
        _translationController.delegate;
    _translationController = [self newTranslationController];
    _translationController.delegate = delegate;
  }

  // Recreate and restore the delegate only if previously lazily loaded.
  if (_autofillController) {
    id<CWVAutofillControllerDelegate> delegate = _autofillController.delegate;
    _autofillController = [self newAutofillController];
    _autofillController.delegate = delegate;
  }

  [self addInternalWebViewAsSubview];

  [self updateNavigationAvailability];
  [self updateCurrentURLs];
  [self updateTitle];
  [self updateVisibleSSLStatus];
  self.loading = NO;
  self.estimatedProgress = 0.0;

  // TODO(crbug.com/41407753): The session will not be restored until
  // LoadIfNecessary call. Fix the bug and remove extra call.
  if (coder) {
    _webState->GetNavigationManager()->LoadIfNecessary();
  }
}

// Adds the web view provided by |_webState| as a subview unless it has already.
- (void)addInternalWebViewAsSubview {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  UIView* subview = _webState->GetView();
  if (subview.superview == self) {
    return;
  }
  subview.frame = self.bounds;
  subview.autoresizingMask =
      UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  [self addSubview:subview];
}

- (CWVBackForwardList*)backForwardList {
  if (![self isWebStateSafeToUse]) {
    return nil;
  }
  if (!_backForwardList) {
    _backForwardList = [[CWVBackForwardList alloc]
        initWithNavigationManager:_webState->GetNavigationManager()];
  }
  return _backForwardList;
}

- (void)updateNavigationAvailability {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  self.backForwardList.navigationManager = _webState->GetNavigationManager();

  self.canGoBack = _webState && _webState->GetNavigationManager()->CanGoBack();
  self.canGoForward =
      _webState && _webState->GetNavigationManager()->CanGoForward();
}

- (void)updateCurrentURLs {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  self.lastCommittedURL = net::NSURLWithGURL(_webState->GetLastCommittedURL());
  self.visibleURL = net::NSURLWithGURL(_webState->GetVisibleURL());
  self.visibleLocationString = base::SysUTF16ToNSString(
      url_formatter::FormatUrlForSecurityDisplay(_webState->GetVisibleURL()));
}

- (void)updateTitle {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  self.title = base::SysUTF16ToNSString(_webState->GetTitle());
}

- (void)updateVisibleSSLStatus {
  if (![self isWebStateSafeToUse]) {
    return;
  }
  web::NavigationItem* visibleItem =
      _webState->GetNavigationManager()->GetVisibleItem();
  if (visibleItem) {
    self.visibleSSLStatus =
        [[CWVSSLStatus alloc] initWithInternalStatus:visibleItem->GetSSL()];
  } else {
    self.visibleSSLStatus = nil;
  }
}

- (void)attachSecurityInterstitialHelpersToWebStateIfNecessary {
  if (![self isWebStateSafeToUse]) {
    return;
  }

  // Lookalike URLs should only be intercepted if handled by the delegate.
  if ([_navigationDelegate respondsToSelector:@selector
                           (webView:handleLookalikeURLWithHandler:)]) {
    LookalikeUrlTabHelper::CreateForWebState(_webState.get());
    LookalikeUrlTabAllowList::CreateForWebState(_webState.get());
    LookalikeUrlContainer::CreateForWebState(_webState.get());
  } else {
    LookalikeUrlTabHelper::RemoveFromWebState(_webState.get());
    LookalikeUrlTabAllowList::RemoveFromWebState(_webState.get());
    LookalikeUrlContainer::RemoveFromWebState(_webState.get());
  }

  // Unsafe URLs should only be intercepted if handled by the delegate.
  if ([_navigationDelegate
          respondsToSelector:@selector(webView:handleUnsafeURLWithHandler:)]) {
    SafeBrowsingClient* client =
        ios_web_view::WebViewSafeBrowsingClientFactory::GetForBrowserState(
            ios_web_view::WebViewBrowserState::FromBrowserState(
                _webState->GetBrowserState()));
    SafeBrowsingQueryManager::CreateForWebState(_webState.get(), client);
    SafeBrowsingTabHelper::CreateForWebState(_webState.get(), client);
    SafeBrowsingUrlAllowList::CreateForWebState(_webState.get());
    SafeBrowsingUnsafeResourceContainer::CreateForWebState(_webState.get());
  } else {
    SafeBrowsingQueryManager::RemoveFromWebState(_webState.get());
    SafeBrowsingTabHelper::RemoveFromWebState(_webState.get());
    SafeBrowsingUrlAllowList::RemoveFromWebState(_webState.get());
    SafeBrowsingUnsafeResourceContainer::RemoveFromWebState(_webState.get());
  }
}

#pragma mark - Internal Methods

- (BOOL)isWebStateSafeToUse {
  return _webState != nil && !_webState->IsBeingDestroyed();
}

- (void)shutDown {
  if (_webState) {
    // To handle the case where -[CWVWebView encodeRestorableStateWithCoder:] is
    // called after this method, precompute the session storage so it may be
    // used during encoding later.
    [_serializationHelper updateStateFromWebState:_webState.get()];

    [self cleanupWebStateReference];
    _webState.reset();
  }
}

- (void)dealloc {
  if (_webState) {
    [self cleanupWebStateReference];
  }
}

- (void)cleanupWebStateReference {
  // CWVBackForwardList is unsafe to use after shutting down.
  _backForwardList.navigationManager = nil;

  if (_webStateObserver) {
    _webState->RemoveObserver(_webStateObserver.get());
    _webStateObserver.reset();
  }
  WebViewHolder::RemoveFromWebState(_webState.get());
}

@end
