// Copyright 2023 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/webapps/browser/installable/installable_data_fetcher.h"

#include "base/functional/callback.h"
#include "components/webapps/browser/installable/installable_logging.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/common/constants.h"
#include "content/public/browser/manifest_icon_downloader.h"
#include "content/public/browser/page.h"
#include "content/public/browser/web_contents.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"

namespace webapps {

InstallableDataFetcher::InstallableDataFetcher(
    content::WebContents* web_contents,
    InstallablePageData& data)
    : web_contents_(web_contents->GetWeakPtr()),
      page_data_(data) {}

InstallableDataFetcher::~InstallableDataFetcher() = default;

void InstallableDataFetcher::FetchManifest(FetcherCallback finish_callback) {
  if (page_data_->manifest_fetched()) {
    // Stop and run the callback if manifest is already fetched.
    std::move(finish_callback).Run(page_data_->manifest_error());
    return;
  }

  // This uses DidFinishNavigation to abort when the primary page changes.
  // Therefore this should always be the correct page.
  web_contents()->GetPrimaryPage().GetManifest(base::BindOnce(
      &InstallableDataFetcher::OnDidGetManifest, weak_ptr_factory_.GetWeakPtr(),
      std::move(finish_callback)));
}

void InstallableDataFetcher::OnDidGetManifest(
    FetcherCallback finish_callback,
    blink::mojom::ManifestRequestResult result,
    const GURL& manifest_url,
    blink::mojom::ManifestPtr manifest) {
  InstallableStatusCode error = InstallableStatusCode::NO_ERROR_DETECTED;
  // An empty manifest signifies an error fetching, parsing, or a
  // frame/CORS/opaque origin related issue. Otherwise the manifest algorithm
  // will always populate default values for `start_url`, `id`, and `scope`.
  if (blink::IsEmptyManifest(manifest) ||
      result == blink::mojom::ManifestRequestResult::kManifestFailedToFetch ||
      result == blink::mojom::ManifestRequestResult::kManifestFailedToParse) {
    error = InstallableStatusCode::MANIFEST_PARSING_OR_NETWORK_ERROR;
  }
  page_data_->OnManifestFetched(std::move(manifest), manifest_url, error);
  std::move(finish_callback).Run(error);
}

void InstallableDataFetcher::FetchWebPageMetadata(
    FetcherCallback finish_callback) {
  if (page_data_->web_page_metadata_fetched()) {
    // Stop and run the callback if metadata is already fetched.
    std::move(finish_callback).Run(InstallableStatusCode::NO_ERROR_DETECTED);
    return;
  }

  // Send a message to the renderer to retrieve information about the page.
  mojo::AssociatedRemote<mojom::WebPageMetadataAgent> metadata_agent;
  web_contents()
      ->GetPrimaryMainFrame()
      ->GetRemoteAssociatedInterfaces()
      ->GetInterface(&metadata_agent);
  // Bind the InterfacePtr into the callback so that it's kept alive until
  // there's either a connection error or a response.
  auto* web_page_metadata_proxy = metadata_agent.get();
  web_page_metadata_proxy->GetWebPageMetadata(
      base::BindOnce(&InstallableDataFetcher::OnDidGetWebPageMetadata,
                     weak_ptr_factory_.GetWeakPtr(), std::move(metadata_agent),
                     std::move(finish_callback)));
}

void InstallableDataFetcher::OnDidGetWebPageMetadata(
    mojo::AssociatedRemote<mojom::WebPageMetadataAgent> metadata_agent,
    FetcherCallback finish_callback,
    mojom::WebPageMetadataPtr web_page_metadata) {
  page_data_->OnPageMetadataFetched(std::move(web_page_metadata));
  std::move(finish_callback).Run(InstallableStatusCode::NO_ERROR_DETECTED);
}

void InstallableDataFetcher::CheckAndFetchBestPrimaryIcon(
    FetcherCallback finish_callback,
    bool prefer_maskable,
    bool fetch_favicon) {
  if (page_data_->manifest_url().is_empty() && !fetch_favicon) {
    std::move(finish_callback).Run(InstallableStatusCode::NO_ERROR_DETECTED);
    return;
  }
  if (page_data_->primary_icon_fetched()) {
    // Stop and run the callback if an icon is already fetched.
    std::move(finish_callback).Run(page_data_->icon_error());
    return;
  }
  icon_fetcher_ = std::make_unique<InstallableIconFetcher>(
      web_contents(), *page_data_, page_data_->GetManifest().icons,
      prefer_maskable, fetch_favicon, std::move(finish_callback));
}

void InstallableDataFetcher::CheckAndFetchScreenshots(
    FetcherCallback finish_callback) {
  if (page_data_->is_screenshots_fetch_complete()) {
    // Stop and run the callback if screenshots was already fetched.
    std::move(finish_callback).Run(InstallableStatusCode::NO_ERROR_DETECTED);
    return;
  }

  screenshots_downloading_ = 0;
  screenshot_complete_ = std::move(finish_callback);

  int num_of_screenshots = 0;
  for (const auto& url : page_data_->GetManifest().screenshots) {
#if BUILDFLAG(IS_ANDROID)
    if (url->form_factor ==
        blink::mojom::ManifestScreenshot::FormFactor::kWide) {
      continue;
    }
#else
    if (url->form_factor !=
        blink::mojom::ManifestScreenshot::FormFactor::kWide) {
      continue;
    }
#endif  // BUILDFLAG(IS_ANDROID)

    if (++num_of_screenshots > kMaximumNumOfScreenshots) {
      break;
    }

    // A screenshot URL that's in the map is already taken care of.
    if (downloaded_screenshots_.count(url->image.src) > 0) {
      continue;
    }

    int ideal_size_in_px = url->image.sizes.empty()
                               ? kMinimumScreenshotSizeInPx
                               : std::max(url->image.sizes[0].width(),
                                          url->image.sizes[0].height());
    // Do not pass in a maximum icon size so that screenshots larger than
    // kMaximumScreenshotSizeInPx are not downscaled to the maximum size by
    // `ManifestIconDownloader::Download`. Screenshots with size larger than
    // kMaximumScreenshotSizeInPx get filtered out by OnScreenshotFetched.
    bool can_download = content::ManifestIconDownloader::Download(
        web_contents(), url->image.src, ideal_size_in_px,
        kMinimumScreenshotSizeInPx,
        /*maximum_icon_size_in_px=*/0,
        base::BindOnce(&InstallableDataFetcher::OnScreenshotFetched,
                       weak_ptr_factory_.GetWeakPtr(), url->image.src),
        /*square_only=*/false);
    if (can_download) {
      ++screenshots_downloading_;
    }
  }

  if (!screenshots_downloading_) {
    page_data_->OnScreenshotsDownloaded(std::vector<Screenshot>());
    std::move(screenshot_complete_)
        .Run(InstallableStatusCode::NO_ERROR_DETECTED);
  }
}

void InstallableDataFetcher::OnScreenshotFetched(GURL screenshot_url,
                                                 const SkBitmap& bitmap) {
  DCHECK_GT(screenshots_downloading_, 0);

  if (!web_contents()) {
    return;
  }

  if (!bitmap.drawsNothing()) {
    downloaded_screenshots_[screenshot_url] = bitmap;
  }

  if (--screenshots_downloading_ == 0) {
    // Now that all images have finished downloading, populate screenshots in
    // the order they are declared in the manifest.
    int num_of_screenshots = 0;
    std::vector<Screenshot> screenshots;
    for (const auto& url : page_data_->GetManifest().screenshots) {
      if (++num_of_screenshots > kMaximumNumOfScreenshots) {
        break;
      }

      auto iter = downloaded_screenshots_.find(url->image.src);
      if (iter == downloaded_screenshots_.end()) {
        continue;
      }

      auto screenshot = iter->second;
      if (screenshot.dimensions().width() > kMaximumScreenshotSizeInPx ||
          screenshot.dimensions().height() > kMaximumScreenshotSizeInPx) {
        continue;
      }

      // Screenshots must have the same aspect ratio. Cross-multiplying
      // dimensions checks portrait vs landscape mode (1:2 vs 2:1 for instance).
      if (screenshots.size() &&
          screenshot.dimensions().width() *
                  screenshots[0].image.dimensions().height() !=
              screenshot.dimensions().height() *
                  screenshots[0].image.dimensions().width()) {
        continue;
      }

      std::pair<int, int> dimensions =
          std::minmax(screenshot.width(), screenshot.height());
      if (dimensions.second > dimensions.first * kMaximumScreenshotRatio) {
        continue;
      }

      screenshots.emplace_back(std::move(screenshot), url->label);
    }

    page_data_->OnScreenshotsDownloaded(std::move(screenshots));
    downloaded_screenshots_.clear();
    std::move(screenshot_complete_)
        .Run(InstallableStatusCode::NO_ERROR_DETECTED);
  }
}

}  // namespace webapps
