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

#include "components/omnibox/browser/favicon_cache.h"

#include <tuple>

#include "base/containers/lru_cache.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "components/favicon/core/favicon_service.h"
#include "components/history/core/browser/history_types.h"
#include "components/omnibox/browser/autocomplete_result.h"
#include "ui/gfx/image/image.h"

namespace {

size_t GetFaviconCacheSize() {
  // Set cache size to twice the number of maximum results in either the
  // on-focus or prefix-suggest mode to avoid favicon refetches as the user
  // types. Favicon fetches are uncached and can hit disk.
  return 2 * std::max(AutocompleteResult::GetMaxMatches(),
                      AutocompleteResult::GetMaxMatches(true));
}

}  // namespace

bool FaviconCache::Request::operator<(const Request& rhs) const {
  // Compare |type| first, and if equal, compare |url|.
  return std::tie(type, url) < std::tie(rhs.type, rhs.url);
}

FaviconCache::FaviconCache(favicon::FaviconService* favicon_service,
                           history::HistoryService* history_service)
    : favicon_service_(favicon_service),
      lru_cache_(GetFaviconCacheSize()),
      responses_without_favicons_(GetFaviconCacheSize()) {
  if (history_service) {
    history_observation_.Observe(history_service);

    favicons_changed_subscription_ =
        history_service->AddFaviconsChangedCallback(base::BindRepeating(
            &FaviconCache::OnFaviconsChanged, weak_factory_.GetWeakPtr()));
  }
}

FaviconCache::~FaviconCache() = default;

gfx::Image FaviconCache::GetFaviconForPageUrl(
    const GURL& page_url,
    FaviconFetchedCallback on_favicon_fetched) {
  return GetFaviconInternal({RequestType::kByPageUrl, page_url},
                            std::move(on_favicon_fetched),
                            /*notify_on_empty=*/false);
}

gfx::Image FaviconCache::GetFaviconForIconUrl(
    const GURL& icon_url,
    FaviconFetchedCallback on_favicon_fetched,
    bool notify_on_empty) {
  return GetFaviconInternal({RequestType::kByIconUrl, icon_url},
                            std::move(on_favicon_fetched), notify_on_empty);
}

gfx::Image FaviconCache::GetFaviconInternal(
    const Request& request,
    FaviconFetchedCallback on_favicon_fetched,
    bool notify_on_empty) {
  if (!favicon_service_)
    return gfx::Image();

  if (request.url.is_empty() || !request.url.is_valid())
    return gfx::Image();

  // Early exit if we have a cached favicon ready.
  auto cache_iterator = lru_cache_.Get(request);
  if (cache_iterator != lru_cache_.end())
    return cache_iterator->second;

  // Early exit if we've already established that we don't have the favicon.
  if (responses_without_favicons_.Peek(request) !=
      responses_without_favicons_.end()) {
    if (notify_on_empty) {
      std::move(on_favicon_fetched).Run(gfx::Image());
    }
    return gfx::Image();
  }

  // We have an outstanding request for this page. Add one more waiting callback
  // and return an empty gfx::Image.
  auto it = pending_requests_.find(request);
  if (it != pending_requests_.end()) {
    it->second.push_back({std::move(on_favicon_fetched), notify_on_empty});
    return gfx::Image();
  }

  switch (request.type) {
    case RequestType::kByPageUrl:
      favicon_service_->GetFaviconImageForPageURL(
          request.url,
          base::BindRepeating(&FaviconCache::OnFaviconFetched,
                              weak_factory_.GetWeakPtr(), request),
          &task_tracker_);
      break;
    case RequestType::kByIconUrl:
      favicon_service_->GetFaviconImage(
          request.url,
          base::BindRepeating(&FaviconCache::OnFaviconFetched,
                              weak_factory_.GetWeakPtr(), request),
          &task_tracker_);
      break;
  }

  pending_requests_[request].push_back(
      {std::move(on_favicon_fetched), notify_on_empty});

  return gfx::Image();
}

void FaviconCache::OnFaviconFetched(
    const Request& request,
    const favicon_base::FaviconImageResult& result) {
  if (result.image.IsEmpty()) {
    responses_without_favicons_.Put(request, true);
    auto it = pending_requests_.find(request);
    CHECK(it != pending_requests_.end());
    for (auto& pending_callback : it->second) {
      if (pending_callback.notify_on_empty) {
        std::move(pending_callback.callback).Run(gfx::Image());
      }
    }
    pending_requests_.erase(it);
    return;
  }

  InvokeRequestCallbackWithFavicon(request, result.image);
}

void FaviconCache::InvokeRequestCallbackWithFavicon(const Request& request,
                                                    const gfx::Image& image) {
  DCHECK(!image.IsEmpty());
  lru_cache_.Put(request, image);

  auto it = pending_requests_.find(request);
  CHECK(it != pending_requests_.end());
  for (auto& pending_callback : it->second) {
    std::move(pending_callback.callback).Run(image);
  }
  pending_requests_.erase(it);
}

void FaviconCache::OnURLVisited(
    history::HistoryService* history_service,
    const history::VisitedURLInfo& visited_url_info) {
  if (visited_url_info.response_code_category ==
      history::VisitResponseCodeCategory::k404) {
    return;
  }
  auto it = responses_without_favicons_.Peek(
      {RequestType::kByPageUrl, visited_url_info.url_row.url()});
  if (it != responses_without_favicons_.end())
    responses_without_favicons_.Erase(it);
}

void FaviconCache::InvalidateCachedRequests(const Request& request) {
  {
    auto it = lru_cache_.Peek(request);
    if (it != lru_cache_.end())
      lru_cache_.Erase(it);
  }

  {
    auto it = responses_without_favicons_.Peek(request);
    if (it != responses_without_favicons_.end())
      responses_without_favicons_.Erase(it);
  }
}

void FaviconCache::OnHistoryDeletions(
    history::HistoryService* history_service,
    const history::DeletionInfo& deletion_info) {
  // We only care about actual user (or sync) deletions.
  if (deletion_info.is_from_expiration())
    return;

  if (deletion_info.IsAllHistory()) {
    lru_cache_.Clear();
    responses_without_favicons_.Clear();
    return;
  }

  for (const history::URLRow& row : deletion_info.deleted_rows()) {
    InvalidateCachedRequests({RequestType::kByPageUrl, row.url()});
  }
}

void FaviconCache::OnFaviconsChanged(const std::set<GURL>& page_urls,
                                     const GURL& icon_url) {
  for (GURL page_url : page_urls) {
    InvalidateCachedRequests({RequestType::kByPageUrl, page_url});
  }

  InvalidateCachedRequests({RequestType::kByIconUrl, icon_url});
}
