// Copyright 2020 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/crash_reporter_url_observer.h"

#import <Foundation/Foundation.h>

#import <array>
#import <map>

#import "base/check.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "components/crash/core/common/crash_key.h"
#import "components/previous_session_info/previous_session_info.h"
#import "ios/chrome/browser/crash_report/model/crash_helper.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/all_web_state_observation_forwarder.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.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/web_state.h"
#import "url/gurl.h"

using crash_reporter::CrashKeyString;

namespace {

// Max number of urls to send. This represent 1 URL per WebState group.
const int kNumberOfURLsToSend = 3;

// Keep the following two CrashKey arrays in sync with `kNumberOfURLsToSend`.
static std::array<crash_reporter::CrashKeyString<1024>, 3> url_crash_keys = {
    crash_reporter::CrashKeyString<1024>{"url0"},
    crash_reporter::CrashKeyString<1024>{"url1"},
    crash_reporter::CrashKeyString<1024>{"url2"},
};
static std::array<CrashKeyString<1024>, 3> pending_url_crash_keys = {
    crash_reporter::CrashKeyString<1024>{"url0-pending"},
    crash_reporter::CrashKeyString<1024>{"url1-pending"},
    crash_reporter::CrashKeyString<1024>{"url2-pending"},
};

// The group for preload WebStates.
const char kPreloadWebStateGroup[] = "PreloadGroup";

}  // namespace

// A CrashReporterParameterSetter that forward parameters to crash keys and
// PreviousSessionInfo.
@interface CrashReporterParameterSetter
    : NSObject <CrashReporterParameterSetter>
@end

@implementation CrashReporterParameterSetter
- (void)removeReportParameter:(NSNumber*)key pending:(BOOL)pending {
  int index = key.intValue;
  DCHECK(index < kNumberOfURLsToSend);
  if (pending) {
    pending_url_crash_keys[index].Clear();
  } else {
    url_crash_keys[index].Clear();
    if (index == 0) {
      // Only sync (and clear) the first non-pending URL to PreviousSessionInfo.
      [[PreviousSessionInfo sharedInstance]
          removeReportParameterForKey:@"url0"];
    }
  }
}
- (void)setReportParameterURL:(const GURL&)URL
                       forKey:(NSNumber*)key
                      pending:(BOOL)pending {
  int index = key.intValue;
  DCHECK(index < kNumberOfURLsToSend);
  if (pending) {
    pending_url_crash_keys[index].Set(URL.spec());
  } else {
    url_crash_keys[index].Set(URL.spec());
    if (index == 0) {
      // Only sync (and clear) the first non-pending URL to PreviousSessionInfo.
      [[PreviousSessionInfo sharedInstance]
          setReportParameterValue:base::SysUTF8ToNSString(URL.spec())
                           forKey:@"url0"];
    }
  }
}
@end

#pragma mark - Life Cycle

// static
CrashReporterURLObserver* CrashReporterURLObserver::GetSharedInstance() {
  static base::NoDestructor<CrashReporterURLObserver> instance(
      [[CrashReporterParameterSetter alloc] init]);
  return instance.get();
}

CrashReporterURLObserver::CrashReporterURLObserver(
    id<CrashReporterParameterSetter> setter) {
  params_setter_ = setter;
  crash_key_by_group_ =
      [[NSMutableDictionary alloc] initWithCapacity:kNumberOfURLsToSend];
  crash_keys_ = [[NSMutableArray alloc] initWithCapacity:kNumberOfURLsToSend];
  for (int i = 0; i < kNumberOfURLsToSend; ++i) {
    [crash_keys_ addObject:[NSNumber numberWithInt:i]];
  }
}

CrashReporterURLObserver::~CrashReporterURLObserver() {
  CHECK(!WebStateListObserver::IsInObserverList() &&
        !web::WebStateObserver::IsInObserverList())
      << "CrashReporterURLObserver needs to be removed from "
         "observer lists before their destruction.";
}

#pragma mark - Group operations

std::string CrashReporterURLObserver::GroupForWebStateList(
    WebStateList* web_state_list) {
  return base::StringPrintf("WebStateList:%p", web_state_list);
}

void CrashReporterURLObserver::RemoveGroup(const std::string& group) {
  NSString* ns_group = base::SysUTF8ToNSString(group);
  NSNumber* key = [crash_key_by_group_ objectForKey:ns_group];
  if (!key) {
    return;
  }
  [params_setter_ removeReportParameter:key pending:NO];
  [params_setter_ removeReportParameter:key pending:YES];
  [crash_key_by_group_ removeObjectForKey:ns_group];
  [crash_keys_ removeObject:key];
  [crash_keys_ insertObject:key atIndex:0];
  current_web_states_.erase(group);
}

void CrashReporterURLObserver::RemoveWebStateList(
    WebStateList* web_state_list) {
  RemoveGroup(GroupForWebStateList(web_state_list));
}

#pragma mark - Record URLs

void CrashReporterURLObserver::RecordURL(const GURL& url,
                                         const web::WebState* web_state,
                                         bool pending) {
  DCHECK(!web_state->GetBrowserState()->IsOffTheRecord());
  std::string group = web_state_to_group_[web_state];
  DCHECK(group.size());
  NSString* ns_group = base::SysUTF8ToNSString(group);
  NSNumber* crash_key = [crash_key_by_group_ objectForKey:ns_group];
  BOOL reusing_key = NO;
  if (!crash_key) {
    // Get the first crash key and push it back at the end of the keys.
    crash_key = [crash_keys_ objectAtIndex:0];

    // Remove the current mapping to the crash key.
    for (NSString* used_group in
         [crash_key_by_group_ allKeysForObject:crash_key]) {
      reusing_key = YES;
      current_web_states_.erase(base::SysNSStringToUTF8(used_group));
      [crash_key_by_group_ removeObjectForKey:used_group];
    }
    // Associate the crash key to the tab id.
    [crash_key_by_group_ setObject:crash_key forKey:ns_group];
  }
  [crash_keys_ removeObject:crash_key];
  [crash_keys_ addObject:crash_key];

  current_web_states_[group] = web_state;
  if (pending) {
    if (reusing_key) {
      [params_setter_ removeReportParameter:crash_key pending:NO];
    }
    [params_setter_ setReportParameterURL:url forKey:crash_key pending:YES];
  } else {
    [params_setter_ setReportParameterURL:url forKey:crash_key pending:NO];
    [params_setter_ removeReportParameter:crash_key pending:YES];
  }
}

void CrashReporterURLObserver::RecordURLForWebState(
    const web::WebState* web_state) {
  // web_state is const, so GetNavigationManager won't force its realization
  // (which is intended).
  const web::NavigationManager* manager = web_state->GetNavigationManager();
  const web::NavigationItem* pending_item =
      manager ? manager->GetPendingItem() : nullptr;
  const GURL& url =
      pending_item ? pending_item->GetURL() : web_state->GetLastCommittedURL();
  RecordURL(url, web_state, pending_item != nullptr);
}

#pragma mark - Start/Stop observing

void CrashReporterURLObserver::ObservePreloadWebState(
    web::WebState* web_state) {
  web_state->AddObserver(this);
  web_state_to_group_[web_state] = kPreloadWebStateGroup;
}

void CrashReporterURLObserver::StopObservingPreloadWebState(
    web::WebState* web_state) {
  web_state->RemoveObserver(this);
  // Check the WebState has not been attached to another group yet.
  if (web_state_to_group_.count(web_state) &&
      web_state_to_group_[web_state] == kPreloadWebStateGroup) {
    web_state_to_group_.erase(web_state);
  }
  if (current_web_states_[kPreloadWebStateGroup] == web_state) {
    RemoveGroup(kPreloadWebStateGroup);
  }
}

void CrashReporterURLObserver::ObserveWebStateList(
    WebStateList* web_state_list) {
  web_state_list->AddObserver(this);
  DCHECK(!all_web_state_observation_forwarders_[web_state_list]);
  // Observe all webStates of this webStateList, so that Tab states are saved in
  // cases of crashing.
  all_web_state_observation_forwarders_[web_state_list] =
      std::make_unique<AllWebStateObservationForwarder>(web_state_list, this);
  for (int i = 0; i < web_state_list->count(); ++i) {
    web::WebState* web_state = web_state_list->GetWebStateAt(i);
    web_state_to_group_[web_state] = GroupForWebStateList(web_state_list);
  }
}

void CrashReporterURLObserver::StopObservingWebStateList(
    WebStateList* web_state_list) {
  std::string group = GroupForWebStateList(web_state_list);
  for (auto it = web_state_to_group_.cbegin();
       it != web_state_to_group_.cend();) {
    if (it->second == group) {
      it = web_state_to_group_.erase(it);
    } else {
      ++it;
    }
  }
  current_web_states_.erase(group);
  RemoveGroup(group);
  all_web_state_observation_forwarders_[web_state_list] = nullptr;
  web_state_list->RemoveObserver(this);
}

#pragma mark - WebStateListObserver

void CrashReporterURLObserver::WebStateListDidChange(
    WebStateList* web_state_list,
    const WebStateListChange& change,
    const WebStateListStatus& status) {
  switch (change.type()) {
    case WebStateListChange::Type::kStatusOnly:
      // The activation is handled after this switch statement.
      break;
    case WebStateListChange::Type::kDetach: {
      const WebStateListChangeDetach& detach_change =
          change.As<WebStateListChangeDetach>();
      web::WebState* detached_web_state = detach_change.detached_web_state();
      web_state_to_group_.erase(detached_web_state);
      if (detached_web_state ==
          current_web_states_[GroupForWebStateList(web_state_list)]) {
        RemoveGroup(GroupForWebStateList(web_state_list));
      }
      break;
    }
    case WebStateListChange::Type::kMove:
      // Do nothing when a WebState is moved.
      break;
    case WebStateListChange::Type::kReplace: {
      const WebStateListChangeReplace& replace_change =
          change.As<WebStateListChangeReplace>();
      web_state_to_group_.erase(replace_change.replaced_web_state());
      web::WebState* inserted_web_state = replace_change.inserted_web_state();
      web_state_to_group_[inserted_web_state] =
          GroupForWebStateList(web_state_list);
      if (web_state_list->GetActiveWebState() == inserted_web_state) {
        RecordURLForWebState(inserted_web_state);
      }
      break;
    }
    case WebStateListChange::Type::kInsert: {
      const WebStateListChangeInsert& insert_change =
          change.As<WebStateListChangeInsert>();
      web::WebState* inserted_web_state = insert_change.inserted_web_state();
      web_state_to_group_[inserted_web_state] =
          GroupForWebStateList(web_state_list);
      if (status.active_web_state_change()) {
        RecordURLForWebState(inserted_web_state);
      }
      break;
    }
    case WebStateListChange::Type::kGroupCreate:
      // Do nothing when a group is created.
      break;
    case WebStateListChange::Type::kGroupVisualDataUpdate:
      // Do nothing when a tab group's visual data are updated.
      break;
    case WebStateListChange::Type::kGroupMove:
      // Do nothing when a tab group is moved.
      break;
    case WebStateListChange::Type::kGroupDelete:
      // Do nothing when a group is deleted.
      break;
  }

  if (status.active_web_state_change() && status.new_active_web_state) {
    // Update WebStateList map in case tabs were moved to another window.
    web_state_to_group_[status.new_active_web_state] =
        GroupForWebStateList(web_state_list);
    RecordURLForWebState(status.new_active_web_state);
  }
}

#pragma mark - WebStateObserver

void CrashReporterURLObserver::DidStartNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  if (navigation_context->GetUrl().spec().empty() ||
      web_state->GetBrowserState()->IsOffTheRecord()) {
    return;
  }
  RecordURL(navigation_context->GetUrl(), web_state, true);
}

void CrashReporterURLObserver::DidFinishNavigation(
    web::WebState* web_state,
    web::NavigationContext* navigation_context) {
  if (navigation_context->GetUrl().spec().empty() ||
      web_state->GetBrowserState()->IsOffTheRecord()) {
    return;
  }
  RecordURL(navigation_context->GetUrl(), web_state, false);
}
