// 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.

#include "chrome/browser/bitmap_fetcher/bitmap_fetcher_service.h"

#include <stddef.h>

#include <memory>
#include <utility>

#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "build/build_config.h"
#include "chrome/browser/bitmap_fetcher/bitmap_fetcher.h"
#include "chrome/browser/image_decoder/image_decoder.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/common/omnibox_features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/load_flags.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "third_party/skia/include/core/SkBitmap.h"

namespace {

const size_t kMaxRequests = 25;  // Maximum number of inflight requests allowed.

// Maximum number of cache entries. This was 5 before, which worked well enough
// for few images like weather answers, but with rich entity suggestions showing
// several images at once, even changing some while the user types, a larger
// cache is necessary to avoid flickering. Each cache entry is expected to take
// 16kb (64x64 @ 32bpp).  With 16, the total memory consumed would be ~256kb.
// 16 is double the default number of maximum suggestions so this can
// accommodate one match image plus one answer image for each result.
#if BUILDFLAG(IS_ANDROID)
// Android caches the images in the java layer.
const int kMaxCacheEntries = 0;
#else
const int kMaxCacheEntries = 16;
#endif

constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("omnibox_result_change", R"(
        semantics {
          sender: "Omnibox"
          description:
            "Chromium provides answers in the suggestion list for "
            "certain queries that user types in the omnibox. This request "
            "retrieves a small image (for example, an icon illustrating "
            "the current weather conditions) when this can add information "
            "to an answer."
          trigger:
            "Change of results for the query typed by the user in the "
            "omnibox."
          data:
            "The only data sent is the path to an image and cookies. User data "
            "might be present in cookies, and some user data might be "
            "inferrable (e.g. whether the weather is sunny or rainy in the "
            "user's current location) from the name of the image in the path. "
            "Requests are sent as same-site: "
            "https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#name-same-site-and-cross-site-re."
          destination: WEBSITE
        }
        policy {
          cookies_allowed: YES
          cookies_store: "user"
          setting:
            "You can enable or disable this feature via 'Use a prediction "
            "service to help complete searches and URLs typed in the "
            "address bar.' in Chromium's settings under Advanced. The "
            "feature is enabled by default."
          chrome_policy {
            SearchSuggestEnabled {
                policy_options {mode: MANDATORY}
                SearchSuggestEnabled: false
            }
          }
        })");

}  // namespace.

class BitmapFetcherRequest {
 public:
  BitmapFetcherRequest(BitmapFetcherService::RequestId request_id,
                       BitmapFetcherService::BitmapFetchedCallback callback);

  BitmapFetcherRequest(const BitmapFetcherRequest&) = delete;
  BitmapFetcherRequest& operator=(const BitmapFetcherRequest&) = delete;

  ~BitmapFetcherRequest();

  void NotifyImageChanged(const SkBitmap* bitmap);
  BitmapFetcherService::RequestId request_id() const { return request_id_; }

  // Weak ptr |fetcher| is used to identify associated fetchers.
  void set_fetcher(const BitmapFetcher* fetcher) { fetcher_ = fetcher; }
  const BitmapFetcher* get_fetcher() const { return fetcher_; }

 private:
  const BitmapFetcherService::RequestId request_id_;
  BitmapFetcherService::BitmapFetchedCallback callback_;
  raw_ptr<const BitmapFetcher> fetcher_;
};

BitmapFetcherRequest::BitmapFetcherRequest(
    BitmapFetcherService::RequestId request_id,
    BitmapFetcherService::BitmapFetchedCallback callback)
    : request_id_(request_id), callback_(std::move(callback)) {}

BitmapFetcherRequest::~BitmapFetcherRequest() = default;

void BitmapFetcherRequest::NotifyImageChanged(const SkBitmap* bitmap) {
  if (bitmap && !bitmap->empty())
    std::move(callback_).Run(*bitmap);
}

BitmapFetcherService::CacheEntry::CacheEntry() = default;

BitmapFetcherService::CacheEntry::~CacheEntry() = default;

BitmapFetcherService::BitmapFetcherService(content::BrowserContext* context)
    : shared_data_decoder_(
          std::make_unique<data_decoder::DataDecoder>(base::Seconds(405))),
      cache_(kMaxCacheEntries),
      current_request_id_(1),
      context_(context) {}

BitmapFetcherService::~BitmapFetcherService() {
  // |active_fetchers_|'s elements must be destructured before
  // |shared_data_decoder_|, as the former contain unowned pointers to the
  // latter.
  requests_.clear();
  active_fetchers_.clear();
}

void BitmapFetcherService::CancelRequest(int request_id) {
  for (auto iter = requests_.begin(); iter != requests_.end(); ++iter) {
    if ((*iter)->request_id() == request_id) {
      requests_.erase(iter);
      // Deliberately leave the associated fetcher running to populate cache.
      return;
    }
  }
}

BitmapFetcherService::RequestId BitmapFetcherService::RequestImageForTesting(
    const GURL& url,
    BitmapFetchedCallback callback,
    const net::NetworkTrafficAnnotationTag& traffic_annotation) {
  return RequestImageImpl(url, std::move(callback), traffic_annotation);
}

BitmapFetcherService::RequestId BitmapFetcherService::RequestImageImpl(
    const GURL& url,
    BitmapFetchedCallback callback,
    const net::NetworkTrafficAnnotationTag& traffic_annotation) {
  // Reject invalid URLs and limit number of simultaneous in-flight requests.
  if (!url.is_valid() || requests_.size() > kMaxRequests) {
    return REQUEST_ID_INVALID;
  }

  // Create a new request, assigning next available request ID.
  ++current_request_id_;
  if (current_request_id_ == REQUEST_ID_INVALID)
    ++current_request_id_;
  int request_id = current_request_id_;
  auto request =
      std::make_unique<BitmapFetcherRequest>(request_id, std::move(callback));

  // Check for existing images first.
  auto iter = cache_.Get(url);
  if (iter != cache_.end()) {
    BitmapFetcherService::CacheEntry* entry = iter->second.get();
    request->NotifyImageChanged(entry->bitmap.get());

    // There is no request ID associated with this - data is already delivered.
    return REQUEST_ID_INVALID;
  }

  // Make sure there's a fetcher for this URL and attach to request.
  const BitmapFetcher* fetcher = EnsureFetcherForUrl(url, traffic_annotation);
  request->set_fetcher(fetcher);

  requests_.push_back(std::move(request));
  return request_id;
}

void BitmapFetcherService::Prefetch(const GURL& url) {
  if (url.is_valid() && !IsCached(url))
    EnsureFetcherForUrl(url, kTrafficAnnotation);
}

bool BitmapFetcherService::IsCached(const GURL& url) {
  return cache_.Get(url) != cache_.end();
}

std::unique_ptr<BitmapFetcher> BitmapFetcherService::CreateFetcher(
    const GURL& url,
    const net::NetworkTrafficAnnotationTag& traffic_annotation) {
  // TODO(https://crbug.com/408008982): Consider merging to `ImageFetcher.`
  std::unique_ptr<BitmapFetcher> new_fetcher = std::make_unique<BitmapFetcher>(
      url, this, traffic_annotation, shared_data_decoder_.get());

  new_fetcher->Init(
      net::ReferrerPolicy::REDUCE_GRANULARITY_ON_TRANSITION_CROSS_ORIGIN,
      network::mojom::CredentialsMode::kInclude, /*additional_headers=*/{},
      /*initiator=*/url::Origin(), /*is_same_site_request=*/true);
  new_fetcher->Start(context_->GetDefaultStoragePartition()
                         ->GetURLLoaderFactoryForBrowserProcess()
                         .get());
  return new_fetcher;
}

BitmapFetcherService::RequestId BitmapFetcherService::RequestImage(
    const GURL& url,
    BitmapFetchedCallback callback) {
  return RequestImageImpl(url, std::move(callback), kTrafficAnnotation);
}

const BitmapFetcher* BitmapFetcherService::EnsureFetcherForUrl(
    const GURL& url,
    const net::NetworkTrafficAnnotationTag& traffic_annotation) {
  const BitmapFetcher* fetcher = FindFetcherForUrl(url);
  if (fetcher)
    return fetcher;

  std::unique_ptr<BitmapFetcher> new_fetcher =
      CreateFetcher(url, traffic_annotation);
  active_fetchers_.push_back(std::move(new_fetcher));
  return active_fetchers_.back().get();
}

const BitmapFetcher* BitmapFetcherService::FindFetcherForUrl(const GURL& url) {
  for (auto it = active_fetchers_.begin(); it != active_fetchers_.end(); ++it) {
    if (url == (*it)->url())
      return it->get();
  }
  return nullptr;
}

void BitmapFetcherService::RemoveFetcher(const BitmapFetcher* fetcher) {
  auto it = active_fetchers_.begin();
  for (; it != active_fetchers_.end(); ++it) {
    if (it->get() == fetcher)
      break;
  }
  // RemoveFetcher should always result in removal.
  CHECK(it != active_fetchers_.end());
  active_fetchers_.erase(it);
}

void BitmapFetcherService::OnFetchComplete(const GURL& url,
                                           const SkBitmap* bitmap) {
  const BitmapFetcher* fetcher = FindFetcherForUrl(url);
  DCHECK(fetcher);

  // Notify all attached requests of completion.
  auto iter = requests_.begin();
  while (iter != requests_.end()) {
    if ((*iter)->get_fetcher() == fetcher) {
      (*iter)->NotifyImageChanged(bitmap);
      iter = requests_.erase(iter);
    } else {
      ++iter;
    }
  }

  if (bitmap && !bitmap->isNull()) {
    std::unique_ptr<CacheEntry> entry(new CacheEntry);
    entry->bitmap = std::make_unique<SkBitmap>(*bitmap);
    cache_.Put(fetcher->url(), std::move(entry));
  }

  RemoveFetcher(fetcher);
}
