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

#include "components/favicon/content/content_favicon_driver.h"

#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "components/favicon/content/favicon_url_util.h"
#include "components/favicon/core/favicon_service.h"
#include "components/favicon/core/favicon_url.h"
#include "components/history/core/browser/history_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/favicon_status.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/common/favicon_url.h"
#include "third_party/blink/public/common/manifest/manifest.h"
#include "ui/gfx/image/image.h"

DEFINE_WEB_CONTENTS_USER_DATA_KEY(favicon::ContentFaviconDriver);

namespace favicon {
namespace {

void ExtractManifestIcons(
    ContentFaviconDriver::ManifestDownloadCallback callback,
    const GURL& manifest_url,
    const blink::Manifest& manifest) {
  std::vector<FaviconURL> candidates;
  for (const auto& icon : manifest.icons) {
    candidates.emplace_back(icon.src, favicon_base::IconType::kWebManifestIcon,
                            icon.sizes);
  }
  std::move(callback).Run(candidates);
}

}  // namespace

// static
void ContentFaviconDriver::CreateForWebContents(
    content::WebContents* web_contents,
    FaviconService* favicon_service,
    history::HistoryService* history_service) {
  if (FromWebContents(web_contents))
    return;

  web_contents->SetUserData(
      UserDataKey(), base::WrapUnique(new ContentFaviconDriver(
                         web_contents, favicon_service, history_service)));
}

void ContentFaviconDriver::SaveFaviconEvenIfInIncognito() {
  content::NavigationEntry* entry =
      web_contents()->GetController().GetLastCommittedEntry();
  if (!entry)
    return;

  // Make sure the page is in history, otherwise adding the favicon does
  // nothing.
  if (!history_service())
    return;
  GURL page_url = entry->GetURL();
  history_service()->AddPageNoVisitForBookmark(page_url, entry->GetTitle());

  const content::FaviconStatus& favicon_status = entry->GetFavicon();
  if (!favicon_service() || !favicon_status.valid ||
      favicon_status.url.is_empty() || favicon_status.image.IsEmpty()) {
    return;
  }

  favicon_service()->SetFavicons({page_url}, favicon_status.url,
                                 favicon_base::IconType::kFavicon,
                                 favicon_status.image);
}

gfx::Image ContentFaviconDriver::GetFavicon() const {
  // Like GetTitle(), we also want to use the favicon for the last committed
  // entry rather than a pending navigation entry.
  const content::NavigationController& controller =
      web_contents()->GetController();
  content::NavigationEntry* entry = controller.GetTransientEntry();
  if (entry)
    return entry->GetFavicon().image;

  entry = controller.GetLastCommittedEntry();
  if (entry)
    return entry->GetFavicon().image;
  return gfx::Image();
}

bool ContentFaviconDriver::FaviconIsValid() const {
  const content::NavigationController& controller =
      web_contents()->GetController();
  content::NavigationEntry* entry = controller.GetTransientEntry();
  if (entry)
    return entry->GetFavicon().valid;

  entry = controller.GetLastCommittedEntry();
  if (entry)
    return entry->GetFavicon().valid;

  return false;
}

GURL ContentFaviconDriver::GetActiveURL() {
  content::NavigationEntry* entry =
      web_contents()->GetController().GetLastCommittedEntry();
  return entry ? entry->GetURL() : GURL();
}

ContentFaviconDriver::ContentFaviconDriver(
    content::WebContents* web_contents,
    FaviconService* favicon_service,
    history::HistoryService* history_service)
    : content::WebContentsObserver(web_contents),
      FaviconDriverImpl(favicon_service, history_service),
      document_on_load_completed_(false) {}

ContentFaviconDriver::~ContentFaviconDriver() {
}

int ContentFaviconDriver::DownloadImage(const GURL& url,
                                        int max_image_size,
                                        ImageDownloadCallback callback) {
  bool bypass_cache = (bypass_cache_page_url_ == GetActiveURL());
  bypass_cache_page_url_ = GURL();

  return web_contents()->DownloadImage(url, true, max_image_size, bypass_cache,
                                       std::move(callback));
}

void ContentFaviconDriver::DownloadManifest(const GURL& url,
                                            ManifestDownloadCallback callback) {
  web_contents()->GetManifest(
      base::BindOnce(&ExtractManifestIcons, std::move(callback)));
}

bool ContentFaviconDriver::IsOffTheRecord() {
  DCHECK(web_contents());
  return web_contents()->GetBrowserContext()->IsOffTheRecord();
}

void ContentFaviconDriver::OnFaviconUpdated(
    const GURL& page_url,
    FaviconDriverObserver::NotificationIconType notification_icon_type,
    const GURL& icon_url,
    bool icon_url_changed,
    const gfx::Image& image) {
  content::NavigationEntry* entry =
      web_contents()->GetController().GetLastCommittedEntry();
  DCHECK(entry && entry->GetURL() == page_url);

  if (notification_icon_type == FaviconDriverObserver::NON_TOUCH_16_DIP) {
    entry->GetFavicon().valid = true;
    entry->GetFavicon().url = icon_url;
    entry->GetFavicon().image = image;
    web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
  }

  NotifyFaviconUpdatedObservers(notification_icon_type, icon_url,
                                icon_url_changed, image);
}

void ContentFaviconDriver::OnFaviconDeleted(
    const GURL& page_url,
    FaviconDriverObserver::NotificationIconType notification_icon_type) {
  content::NavigationEntry* entry =
      web_contents()->GetController().GetLastCommittedEntry();
  DCHECK(entry && entry->GetURL() == page_url);

  if (notification_icon_type == FaviconDriverObserver::NON_TOUCH_16_DIP) {
    entry->GetFavicon() = content::FaviconStatus();
    web_contents()->NotifyNavigationStateChanged(content::INVALIDATE_TYPE_TAB);
  }

  NotifyFaviconUpdatedObservers(notification_icon_type, /*icon_url=*/GURL(),
                                /*icon_url_changed=*/true,
                                content::FaviconStatus().image);
}

void ContentFaviconDriver::DidUpdateFaviconURL(
    const std::vector<content::FaviconURL>& candidates) {
  // Ignore the update if there is no last committed navigation entry. This can
  // occur when loading an initially blank page.
  content::NavigationEntry* entry =
      web_contents()->GetController().GetLastCommittedEntry();
  if (!entry)
    return;

  // We update |favicon_urls_| even if the list is believed to be partial
  // (checked below), because callers of our getter favicon_urls() expect so.
  favicon_urls_ = candidates;

  if (!document_on_load_completed_)
    return;

  OnUpdateCandidates(entry->GetURL(),
                     FaviconURLsFromContentFaviconURLs(candidates),
                     manifest_url_);
}

void ContentFaviconDriver::DidUpdateWebManifestURL(
    const base::Optional<GURL>& manifest_url) {
  // Ignore the update if there is no last committed navigation entry. This can
  // occur when loading an initially blank page.
  content::NavigationEntry* entry =
      web_contents()->GetController().GetLastCommittedEntry();
  if (!entry || !document_on_load_completed_)
    return;

  manifest_url_ = manifest_url.value_or(GURL());

  // On regular page loads, DidUpdateManifestURL() is guaranteed to be called
  // before DidUpdateFaviconURL(). However, a page can update the favicons via
  // javascript.
  if (favicon_urls_.has_value()) {
    OnUpdateCandidates(entry->GetURL(),
                       FaviconURLsFromContentFaviconURLs(*favicon_urls_),
                       manifest_url_);
  }
}

void ContentFaviconDriver::DidStartNavigation(
    content::NavigationHandle* navigation_handle) {
  if (!navigation_handle->IsInMainFrame())
    return;

  favicon_urls_.reset();

  if (!navigation_handle->IsSameDocument()) {
    document_on_load_completed_ = false;
    manifest_url_ = GURL();
  }

  content::ReloadType reload_type = navigation_handle->GetReloadType();
  if (reload_type == content::ReloadType::NONE || IsOffTheRecord())
    return;

  bypass_cache_page_url_ = navigation_handle->GetURL();
  SetFaviconOutOfDateForPage(
      navigation_handle->GetURL(),
      reload_type == content::ReloadType::BYPASSING_CACHE);
}

void ContentFaviconDriver::DidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  if (!navigation_handle->IsInMainFrame() ||
      !navigation_handle->HasCommitted() ||
      navigation_handle->IsErrorPage()) {
    return;
  }

  // Wait till the user navigates to a new URL to start checking the cache
  // again. The cache may be ignored for non-reload navigations (e.g.
  // history.replace() in-page navigation). This is allowed to increase the
  // likelihood that "reloading a page ignoring the cache" redownloads the
  // favicon. In particular, a page may do an in-page navigation before
  // FaviconHandler has the time to determine that the favicon needs to be
  // redownloaded.
  GURL url = navigation_handle->GetURL();
  if (url != bypass_cache_page_url_)
    bypass_cache_page_url_ = GURL();

  // Get the favicon, either from history or request it from the net.
  FetchFavicon(url, navigation_handle->IsSameDocument());
}

void ContentFaviconDriver::DocumentOnLoadCompletedInMainFrame() {
  document_on_load_completed_ = true;
}

}  // namespace favicon
