// Copyright 2019 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/crash_report/model/breadcrumbs/breadcrumb_manager_tab_helper.h"

#import "base/check.h"
#import "base/functional/callback_helpers.h"
#import "base/ios/ns_error_util.h"
#import "base/strings/stringprintf.h"
#import "components/breadcrumbs/core/breadcrumb_manager_keyed_service.h"
#import "ios/chrome/browser/crash_report/model/breadcrumbs/breadcrumb_manager_keyed_service_factory.h"
#import "ios/chrome/browser/infobars/model/infobar_manager_impl.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/url/url_util.h"
#import "ios/net/protocol_handler_util.h"
#import "ios/web/public/favicon/favicon_url.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/security/security_style.h"
#import "ios/web/public/security/ssl_status.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_state.h"
#import "ios/web/public/web_state_id.h"

using LoggingBlock = void (^)(const std::string& event);

// Observes scroll and zoom events and executes LoggingBlock.
@interface BreadcrumbScrollingObserver
    : NSObject <CRWWebViewScrollViewProxyObserver>
- (instancetype)initWithLoggingBlock:(LoggingBlock)loggingBlock;
@end
@implementation BreadcrumbScrollingObserver {
  LoggingBlock _loggingBlock;
}

- (instancetype)initWithLoggingBlock:(LoggingBlock)loggingBlock {
  if ((self = [super init])) {
    _loggingBlock = loggingBlock;
  }
  return self;
}

- (void)webViewScrollViewDidEndDragging:
            (CRWWebViewScrollViewProxy*)webViewScrollViewProxy
                         willDecelerate:(BOOL)decelerate {
  _loggingBlock(breadcrumbs::kBreadcrumbScroll);
}

- (void)webViewScrollViewDidEndZooming:
            (CRWWebViewScrollViewProxy*)webViewScrollViewProxy
                               atScale:(CGFloat)scale {
  _loggingBlock(breadcrumbs::kBreadcrumbZoom);
}

@end

BreadcrumbManagerTabHelper::BreadcrumbManagerTabHelper(web::WebState* web_state)
    : breadcrumbs::BreadcrumbManagerTabHelper(
          InfoBarManagerImpl::FromWebState(web_state),
          web_state->GetUniqueIdentifier().identifier()),
      web_state_(web_state) {
  // Assert that the WebState unique identifier can be used as unique id.
  static_assert(
      std::is_same<decltype(web_state->GetUniqueIdentifier().identifier()),
                   decltype(this->GetUniqueId())>::value);

  CHECK(web_state_->IsRealized());
  web_state_->AddObserver(this);
  base::RepeatingCallback callback =
      base::BindRepeating(&BreadcrumbManagerTabHelper::OnScrollEvent,
                          weak_ptr_factory_.GetWeakPtr());

  scroll_observer_ = [[BreadcrumbScrollingObserver alloc]
      initWithLoggingBlock:base::CallbackToBlock(callback)];
  [web_state_->GetWebViewProxy().scrollViewProxy addObserver:scroll_observer_];
}

BreadcrumbManagerTabHelper::~BreadcrumbManagerTabHelper() = default;

void BreadcrumbManagerTabHelper::PlatformLogEvent(const std::string& event) {
  const bool is_scroll_event = event.contains(breadcrumbs::kBreadcrumbScroll);
  if (!is_scroll_event) {
    // `sequentially_scrolled_` is incremented for each scroll event and reset
    // here when non-scrolling event is logged. The user can scroll multiple
    // times and `sequentially_scrolled_` will allow to throttle the logs to
    // avoid polluting breadcrumbs.
    sequentially_scrolled_ = 0;
  }

  BreadcrumbManagerKeyedServiceFactory::GetForProfile(
      ProfileIOS::FromBrowserState(web_state_->GetBrowserState()))
      ->AddEvent(event);
}

void BreadcrumbManagerTabHelper::DidStartNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  LogDidStartNavigation(navigation_context->GetNavigationId(),
                        navigation_context->GetUrl(),
                        IsUrlNtp(navigation_context->GetUrl()),
                        navigation_context->IsRendererInitiated(),
                        navigation_context->HasUserGesture(),
                        navigation_context->GetPageTransition());
}

void BreadcrumbManagerTabHelper::DidFinishNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  NSError* error = navigation_context->GetError();
  int error_code = 0;
  if (error) {
    error_code = net::ERR_FAILED;
    NSError* final_error = base::ios::GetFinalUnderlyingErrorFromError(error);
    // Only errors with net::kNSErrorDomain have correct net error code.
    if (final_error &&
        [final_error.domain isEqualToString:net::kNSErrorDomain]) {
      error_code = final_error.code;
    }
  }
  LogDidFinishNavigation(navigation_context->GetNavigationId(),
                         navigation_context->IsDownload(), error_code);
}

void BreadcrumbManagerTabHelper::PageLoaded(
    web::WebState* web_state,
    web::PageLoadCompletionStatus load_completion_status) {
  LogPageLoaded(
      IsUrlNtp(web_state->GetLastCommittedURL()),
      web_state->GetLastCommittedURL(),
      load_completion_status == web::PageLoadCompletionStatus::SUCCESS,
      web_state->GetContentsMimeType());
}

void BreadcrumbManagerTabHelper::DidChangeVisibleSecurityState(
    web::WebState* web_state) {
  web::NavigationItem* visible_item =
      web_state->GetNavigationManager()->GetVisibleItem();
  if (!visible_item) {
    return;
  }
  const web::SSLStatus& ssl = visible_item->GetSSL();

  const bool displayed_mixed_content =
      ssl.content_status & web::SSLStatus::DISPLAYED_INSECURE_CONTENT;
  const bool security_style_authentication_broken =
      ssl.security_style == web::SECURITY_STYLE_AUTHENTICATION_BROKEN;

  LogDidChangeVisibleSecurityState(displayed_mixed_content,
                                   security_style_authentication_broken);
}

void BreadcrumbManagerTabHelper::RenderProcessGone(web::WebState* web_state) {
  LogRenderProcessGone();
}

void BreadcrumbManagerTabHelper::WebStateDestroyed(web::WebState* web_state) {
  web_state->RemoveObserver(this);

  [[web_state->GetWebViewProxy() scrollViewProxy]
      removeObserver:scroll_observer_];
  scroll_observer_ = nil;
  web_state_ = nil;
}

void BreadcrumbManagerTabHelper::OnScrollEvent(const std::string& event) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (event == breadcrumbs::kBreadcrumbScroll) {
    sequentially_scrolled_++;
    if (ShouldLogRepeatedEvent(sequentially_scrolled_)) {
      LogEvent(base::StringPrintf("%s %d", breadcrumbs::kBreadcrumbScroll,
                                  sequentially_scrolled_));
    }
  } else {
    LogEvent(event);
  }
}
