// 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 "ios/web/navigation/navigation_item_impl.h"

#import <stddef.h>

#import <memory>
#import <utility>

#import "base/check_op.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "components/url_formatter/url_formatter.h"
#import "ios/web/common/features.h"
#import "ios/web/navigation/navigation_manager_impl.h"
#import "ios/web/navigation/proto_util.h"
#import "ios/web/navigation/wk_navigation_util.h"
#import "ios/web/public/session/proto/navigation.pb.h"
#import "ios/web/public/session/proto/proto_util.h"
#import "ios/web/public/web_client.h"
#import "ui/base/page_transition_types.h"
#import "ui/gfx/text_elider.h"

namespace web {
namespace {

// Returns a new unique ID for use in NavigationItem during construction.  The
// returned ID is guaranteed to be nonzero (which is the "no ID" indicator).
static int GetUniqueIDInConstructor() {
  static int unique_id_counter = 0;
  return ++unique_id_counter;
}

// Returns whether `referrer` needs to be serialized.
bool ShouldSerializeReferrer(const Referrer& referrer) {
  return referrer.url.is_valid() &&
         referrer.url.spec().size() < url::kMaxURLChars;
}

}  // namespace

using HttpRequestHeaders = NavigationItem::HttpRequestHeaders;

// Value 512 was picked as a tradeoff between saving memory from excessively
// long titles, while preserving the entire title as often as possible for
// features like Sync, where titles can be shared to other platforms that
// have UI surfaces supporting longer titles than iOS.
const size_t kMaxTitleLength = 512;

// static
std::unique_ptr<NavigationItem> NavigationItem::Create() {
  return std::unique_ptr<NavigationItem>(new NavigationItemImpl());
}

NavigationItemImpl::NavigationItemImpl()
    : unique_id_(GetUniqueIDInConstructor()) {}

NavigationItemImpl::~NavigationItemImpl() {}

NavigationItemImpl::NavigationItemImpl(
    const proto::NavigationItemStorage& storage)
    : unique_id_(GetUniqueIDInConstructor()),
      referrer_(ReferrerFromProto(storage.referrer())),
      title_(base::UTF8ToUTF16(storage.title())),
      // Use reload transition type to avoid incorrect increase for other
      // transition types (such as typed).
      transition_type_(ui::PAGE_TRANSITION_RELOAD),
      timestamp_(TimeFromProto(storage.timestamp())),
      user_agent_type_(UserAgentTypeFromProto(storage.user_agent())),
      http_request_headers_(
          HttpRequestHeadersFromProto(storage.http_request_headers())),
      was_created_automatically_(storage.was_created_automatically()) {
  if (!storage.internal_scroll_to_text_fragment().empty()) {
    internal_scroll_to_text_fragment_ =
        storage.internal_scroll_to_text_fragment();
  }
  // While the virtual URL is persisted, the original request URL and the
  // non-virtual URL needs to be set upon NavigationItem creation. Since
  // GetVirtualURL() returns `url_` for the non-overridden case, this will
  // also update the virtual URL reported by this object.
  url_ = original_request_url_ = GURL(storage.url());

  if (!storage.security_scoped_file_resource().empty()) {
    const std::string& bytes = storage.security_scoped_file_resource();
    security_scoped_file_resource_ = [NSData dataWithBytes:bytes.data()
                                                    length:bytes.size()];
  }

  // Restore the `virtual_url`. In case the `url` is invalid, it should be set
  // to the the value saved for `virtual_url` (we never store `virtual_url` if
  // equal to `url`, so restore to `url` only in that case).
  const GURL virtual_url(storage.virtual_url());
  if (virtual_url.is_valid()) {
    if (!url_.is_valid()) {
      url_ = virtual_url;
    } else {
      if (virtual_url != url_) {
        virtual_url_ = virtual_url;
      }
    }
  }
}

void NavigationItemImpl::SerializeToProto(
    proto::NavigationItemStorage& storage) const {
  if (url_.is_valid()) {
    storage.set_url(url_.spec());
  }
  if (url_ != virtual_url_ && virtual_url_.is_valid()) {
    storage.set_virtual_url(virtual_url_.spec());
  }
  if (!title_.empty()) {
    storage.set_title(base::UTF16ToUTF8(title_));
  }
  SerializeTimeToProto(timestamp_, *storage.mutable_timestamp());
  storage.set_user_agent(UserAgentTypeToProto(user_agent_type_));
  if (ShouldSerializeReferrer(referrer_)) {
    SerializeReferrerToProto(referrer_, *storage.mutable_referrer());
  }
  if (http_request_headers_.count) {
    SerializeHttpRequestHeadersToProto(http_request_headers_,
                                       *storage.mutable_http_request_headers());
  }
  if (security_scoped_file_resource_) {
    storage.set_security_scoped_file_resource(
        static_cast<const char*>(security_scoped_file_resource_.bytes),
        security_scoped_file_resource_.length);
  }
  if (was_created_automatically_) {
    storage.set_was_created_automatically(was_created_automatically_);
  }
  if (internal_scroll_to_text_fragment_.has_value()) {
    storage.set_internal_scroll_to_text_fragment(
        internal_scroll_to_text_fragment_.value());
  }
}

std::unique_ptr<NavigationItemImpl> NavigationItemImpl::Clone() {
  return base::WrapUnique(new NavigationItemImpl(*this));
}

int NavigationItemImpl::GetUniqueID() const {
  return unique_id_;
}

void NavigationItemImpl::SetOriginalRequestURL(const GURL& url) {
  original_request_url_ = url;
}

const GURL& NavigationItemImpl::GetOriginalRequestURL() const {
  return original_request_url_;
}

void NavigationItemImpl::SetURL(const GURL& url) {
  url_ = url;
  cached_display_title_.clear();
}

const GURL& NavigationItemImpl::GetURL() const {
  return url_;
}

void NavigationItemImpl::SetReferrer(const web::Referrer& referrer) {
  referrer_ = referrer;
}

const web::Referrer& NavigationItemImpl::GetReferrer() const {
  return referrer_;
}

void NavigationItemImpl::SetVirtualURL(const GURL& url) {
  virtual_url_ = (url == url_) ? GURL() : url;
  cached_display_title_.clear();
}

const GURL& NavigationItemImpl::GetVirtualURL() const {
  return virtual_url_.is_empty() ? url_ : virtual_url_;
}

void NavigationItemImpl::SetTitle(const std::u16string& title) {
  if (title_ == title) {
    return;
  }

  if (title.size() > kMaxTitleLength) {
    title_ = gfx::TruncateString(title, kMaxTitleLength, gfx::CHARACTER_BREAK);
  } else {
    title_ = title;
  }
  cached_display_title_.clear();
}

const std::u16string& NavigationItemImpl::GetTitle() const {
  return title_;
}

void NavigationItemImpl::SetInternalScrollToTextFragment(
    const std::optional<std::string>& internal_scroll_to_text_fragment) {
  internal_scroll_to_text_fragment_ = internal_scroll_to_text_fragment;
}

const std::optional<std::string>&
NavigationItemImpl::GetInternalScrollToTextFragment() const {
  return internal_scroll_to_text_fragment_;
}

const std::u16string& NavigationItemImpl::GetTitleForDisplay() const {
  // Most pages have real titles. Don't even bother caching anything if this is
  // the case.
  if (!title_.empty()) {
    return title_;
  }

  // More complicated cases will use the URLs as the title. This result we will
  // cache since it's more complicated to compute.
  if (!cached_display_title_.empty()) {
    return cached_display_title_;
  }

  // File urls have different display rules, so use one if it is present.
  cached_display_title_ = NavigationItemImpl::GetDisplayTitleForURL(
      GetURL().SchemeIsFile() ? GetURL() : GetVirtualURL());
  return cached_display_title_;
}

void NavigationItemImpl::SetTransitionType(ui::PageTransition transition_type) {
  transition_type_ = transition_type;
}

ui::PageTransition NavigationItemImpl::GetTransitionType() const {
  return transition_type_;
}

const FaviconStatus& NavigationItemImpl::GetFaviconStatus() const {
  return favicon_status_;
}

void NavigationItemImpl::SetFaviconStatus(const FaviconStatus& favicon_status) {
  favicon_status_ = favicon_status;
}

const SSLStatus& NavigationItemImpl::GetSSL() const {
  return ssl_;
}

SSLStatus& NavigationItemImpl::GetSSL() {
  return ssl_;
}

void NavigationItemImpl::SetTimestamp(base::Time timestamp) {
  timestamp_ = timestamp;
}

base::Time NavigationItemImpl::GetTimestamp() const {
  return timestamp_;
}

void NavigationItemImpl::SetUserAgentType(UserAgentType type) {
  user_agent_type_ = type;
}

void NavigationItemImpl::SetSecurityScopedFileResource(NSData* data) {
  security_scoped_file_resource_ = [data copy];
}

NSData* NavigationItemImpl::GetSecurityScopedFileResource() {
  return security_scoped_file_resource_;
}

void NavigationItemImpl::SetUntrusted() {
  is_untrusted_ = true;
}

bool NavigationItemImpl::IsUntrusted() {
  return is_untrusted_;
}

UserAgentType NavigationItemImpl::GetUserAgentType() const {
  return user_agent_type_;
}

bool NavigationItemImpl::HasPostData() const {
  return post_data_ != nil;
}

HttpRequestHeaders* NavigationItemImpl::GetHttpRequestHeaders() const {
  return [http_request_headers_ copy];
}

void NavigationItemImpl::AddHttpRequestHeaders(
    HttpRequestHeaders* additional_headers) {
  if (!additional_headers) {
    return;
  }

  if (http_request_headers_) {
    [http_request_headers_ addEntriesFromDictionary:additional_headers];
  } else {
    http_request_headers_ = [additional_headers mutableCopy];
  }
}

void NavigationItemImpl::SetHttpsUpgradeType(
    HttpsUpgradeType https_upgrade_type) {
  https_upgrade_type_ = https_upgrade_type;
}

HttpsUpgradeType NavigationItemImpl::GetHttpsUpgradeType() const {
  return https_upgrade_type_;
}

void NavigationItemImpl::SetSerializedStateObject(
    NSString* serialized_state_object) {
  serialized_state_object_ = serialized_state_object;
}

NSString* NavigationItemImpl::GetSerializedStateObject() const {
  return serialized_state_object_;
}

void NavigationItemImpl::SetNavigationInitiationType(
    web::NavigationInitiationType navigation_initiation_type) {
  navigation_initiation_type_ = navigation_initiation_type;
}

web::NavigationInitiationType NavigationItemImpl::NavigationInitiationType()
    const {
  return navigation_initiation_type_;
}

void NavigationItemImpl::SetIsCreatedFromHashChange(bool hash_change) {
  is_created_from_hash_change_ = hash_change;
}

bool NavigationItemImpl::IsCreatedFromHashChange() const {
  return is_created_from_hash_change_;
}

void NavigationItemImpl::SetWasCreatedAutomatically(bool value) {
  was_created_automatically_ = value;
}

bool NavigationItemImpl::WasCreatedAutomatically() const {
  return was_created_automatically_;
}

void NavigationItemImpl::SetShouldSkipSerialization(bool skip) {
  should_skip_serialization_ = skip;
}

bool NavigationItemImpl::ShouldSkipSerialization() const {
  return should_skip_serialization_ || url_.spec().size() > url::kMaxURLChars;
}

void NavigationItemImpl::SetPostData(NSData* post_data) {
  post_data_ = post_data;
}

NSData* NavigationItemImpl::GetPostData() const {
  return post_data_;
}

void NavigationItemImpl::RemoveHttpRequestHeaderForKey(NSString* key) {
  DCHECK(key);
  [http_request_headers_ removeObjectForKey:key];
  if (![http_request_headers_ count]) {
    http_request_headers_ = nil;
  }
}

void NavigationItemImpl::ResetHttpRequestHeaders() {
  http_request_headers_ = nil;
}

void NavigationItemImpl::ResetForCommit() {
  // Navigation initiation type is only valid for pending navigations, thus
  // always reset to NONE after the item is committed.
  SetNavigationInitiationType(web::NavigationInitiationType::NONE);
}

void NavigationItemImpl::RestoreStateFromItem(NavigationItem* other) {
  // Restore the UserAgent type in any case, as if the URLs are different it
  // might mean that `this` is a next navigation. The page display state and the
  // virtual URL only make sense if it is the same item. The other headers might
  // not make sense after creating a new navigation to the page.
  if (other->GetUserAgentType() != UserAgentType::NONE) {
    SetUserAgentType(other->GetUserAgentType());
  }
  if (url_ == other->GetURL()) {
    SetVirtualURL(other->GetVirtualURL());
    SetSecurityScopedFileResource(other->GetSecurityScopedFileResource());
  }
}

// static
std::u16string NavigationItemImpl::GetDisplayTitleForURL(const GURL& url) {
  if (url.is_empty()) {
    return std::u16string();
  }

  std::u16string title = url_formatter::FormatUrl(url);

  // For file:// URLs use the filename as the title, not the full path.
  if (url.SchemeIsFile()) {
    std::u16string::size_type slashpos = title.rfind('/');
    if (slashpos != std::u16string::npos && slashpos != (title.size() - 1)) {
      title = title.substr(slashpos + 1);
    }
  }

  const size_t kMaxTitleChars = 4 * 1024;
  gfx::ElideString(title, kMaxTitleChars, &title);
  return title;
}

#ifndef NDEBUG
NSString* NavigationItemImpl::GetDescription() const {
  return [NSString
      stringWithFormat:
          @"url:%s virtual_url_:%s originalurl:%s referrer: %s title:%s "
          @"transition:%d userAgent:%s "
           "is_created_from_hash_change: %@ "
           "navigation_initiation_type: %d "
           "https_upgrade_type: %s",
          url_.spec().c_str(), virtual_url_.spec().c_str(),
          original_request_url_.spec().c_str(), referrer_.url.spec().c_str(),
          base::UTF16ToUTF8(title_).c_str(), transition_type_,
          GetUserAgentTypeDescription(user_agent_type_).c_str(),
          is_created_from_hash_change_ ? @"true" : @"false",
          static_cast<int>(navigation_initiation_type_),
          GetHttpsUpgradeTypeDescription(https_upgrade_type_).c_str()];
}
#endif

NavigationItemImpl::NavigationItemImpl(const NavigationItemImpl& item)
    : unique_id_(item.unique_id_),
      original_request_url_(item.original_request_url_),
      url_(item.url_),
      referrer_(item.referrer_),
      virtual_url_(item.virtual_url_),
      internal_scroll_to_text_fragment_(item.internal_scroll_to_text_fragment_),
      title_(item.title_),
      transition_type_(item.transition_type_),
      favicon_status_(item.favicon_status_),
      ssl_(item.ssl_),
      timestamp_(item.timestamp_),
      user_agent_type_(item.user_agent_type_),
      http_request_headers_([item.http_request_headers_ mutableCopy]),
      serialized_state_object_([item.serialized_state_object_ copy]),
      is_created_from_hash_change_(item.is_created_from_hash_change_),
      should_skip_serialization_(item.should_skip_serialization_),
      post_data_([item.post_data_ copy]),
      navigation_initiation_type_(item.navigation_initiation_type_),
      is_untrusted_(item.is_untrusted_),
      cached_display_title_(item.cached_display_title_),
      https_upgrade_type_(item.https_upgrade_type_) {
  CloneDataFrom(item);
}

}  // namespace web
