// Copyright (c) 2012 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 "chrome/browser/ui/webui/extensions/extension_icon_source.h"

#include <stddef.h>

#include <memory>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/ref_counted_memory.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/component_extension_resources_map.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/image_loader.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_resource.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/grit/extensions_browser_resources.h"
#include "skia/ext/image_operations.h"
#include "ui/base/layout.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/skbitmap_operations.h"
#include "url/gurl.h"

namespace extensions {

namespace {

scoped_refptr<base::RefCountedMemory> BitmapToMemory(const SkBitmap* image) {
  base::RefCountedBytes* image_bytes = new base::RefCountedBytes;
  gfx::PNGCodec::EncodeBGRASkBitmap(*image, false, &image_bytes->data());
  return image_bytes;
}

SkBitmap DesaturateImage(const SkBitmap* image) {
  color_utils::HSL shift = {-1, 0, 0.6};
  return SkBitmapOperations::CreateHSLShiftedBitmap(*image, shift);
}

SkBitmap* ToBitmap(const unsigned char* data, size_t size) {
  SkBitmap* decoded = new SkBitmap();
  bool success = gfx::PNGCodec::Decode(data, size, decoded);
  DCHECK(success);
  return decoded;
}

}  // namespace

ExtensionIconSource::ExtensionIconSource(Profile* profile) : profile_(profile) {
}

struct ExtensionIconSource::ExtensionIconRequest {
  content::URLDataSource::GotDataCallback callback;
  scoped_refptr<const Extension> extension;
  bool grayscale;
  int size;
  ExtensionIconSet::MatchType match;
};

// static
GURL ExtensionIconSource::GetIconURL(const Extension* extension,
                                     int icon_size,
                                     ExtensionIconSet::MatchType match,
                                     bool grayscale) {
  GURL icon_url(base::StringPrintf("%s%s/%d/%d%s",
                                   chrome::kChromeUIExtensionIconURL,
                                   extension->id().c_str(),
                                   icon_size,
                                   match,
                                   grayscale ? "?grayscale=true" : ""));
  CHECK(icon_url.is_valid());
  return icon_url;
}

// static
SkBitmap* ExtensionIconSource::LoadImageByResourceId(int resource_id) {
  base::StringPiece contents =
      ui::ResourceBundle::GetSharedInstance().GetRawDataResourceForScale(
          resource_id, ui::SCALE_FACTOR_100P);

  // Convert and return it.
  const unsigned char* data =
      reinterpret_cast<const unsigned char*>(contents.data());
  return ToBitmap(data, contents.length());
}

std::string ExtensionIconSource::GetSource() {
  return chrome::kChromeUIExtensionIconHost;
}

std::string ExtensionIconSource::GetMimeType(const std::string&) {
  // We need to explicitly return a mime type, otherwise if the user tries to
  // drag the image they get no extension.
  return "image/png";
}

void ExtensionIconSource::StartDataRequest(
    const std::string& path,
    const content::ResourceRequestInfo::WebContentsGetter& wc_getter,
    const content::URLDataSource::GotDataCallback& callback) {
  // This is where everything gets started. First, parse the request and make
  // the request data available for later.
  static int next_id = 0;
  if (!ParseData(path, ++next_id, callback)) {
    // If the request data cannot be parsed, request parameters will not be
    // added to |request_map_|.
    // Send back the default application icon (not resized or desaturated) as
    // the default response.
    callback.Run(BitmapToMemory(GetDefaultAppImage()).get());
    return;
  }

  ExtensionIconRequest* request = GetData(next_id);
  ExtensionResource icon = IconsInfo::GetIconResource(
      request->extension.get(), request->size, request->match);

  if (icon.relative_path().empty()) {
    LoadIconFailed(next_id);
  } else {
    LoadExtensionImage(icon, next_id);
  }
}

bool ExtensionIconSource::AllowCaching() {
  // Should not be cached to reflect the latest contents that may be updated by
  // Extensions.
  return false;
}

ExtensionIconSource::~ExtensionIconSource() {
}

const SkBitmap* ExtensionIconSource::GetDefaultAppImage() {
  if (!default_app_data_.get())
    default_app_data_.reset(LoadImageByResourceId(IDR_APP_DEFAULT_ICON));

  return default_app_data_.get();
}

const SkBitmap* ExtensionIconSource::GetDefaultExtensionImage() {
  if (!default_extension_data_.get()) {
    default_extension_data_.reset(
        LoadImageByResourceId(IDR_EXTENSION_DEFAULT_ICON));
  }

  return default_extension_data_.get();
}

void ExtensionIconSource::FinalizeImage(const SkBitmap* image,
                                        int request_id) {
  SkBitmap bitmap;
  ExtensionIconRequest* request = GetData(request_id);
  if (request->grayscale)
    bitmap = DesaturateImage(image);
  else
    bitmap = *image;

  request->callback.Run(BitmapToMemory(&bitmap).get());
  ClearData(request_id);
}

void ExtensionIconSource::LoadDefaultImage(int request_id) {
  ExtensionIconRequest* request = GetData(request_id);
  const SkBitmap* default_image = NULL;

  if (request->extension->is_app())
    default_image = GetDefaultAppImage();
  else
    default_image = GetDefaultExtensionImage();

  SkBitmap resized_image(skia::ImageOperations::Resize(
      *default_image, skia::ImageOperations::RESIZE_LANCZOS3,
      request->size, request->size));

  // There are cases where Resize returns an empty bitmap, for example if you
  // ask for an image too large. In this case it is better to return the default
  // image than returning nothing at all.
  if (resized_image.empty())
    resized_image = *default_image;

  FinalizeImage(&resized_image, request_id);
}

void ExtensionIconSource::LoadExtensionImage(const ExtensionResource& icon,
                                             int request_id) {
  ExtensionIconRequest* request = GetData(request_id);
  ImageLoader::Get(profile_)->LoadImageAsync(
      request->extension.get(), icon, gfx::Size(request->size, request->size),
      base::BindOnce(&ExtensionIconSource::OnImageLoaded, AsWeakPtr(),
                     request_id));
}

void ExtensionIconSource::LoadFaviconImage(int request_id) {
  favicon::FaviconService* favicon_service =
      FaviconServiceFactory::GetForProfile(profile_,
                                           ServiceAccessType::EXPLICIT_ACCESS);
  // Fall back to the default icons if the service isn't available.
  if (favicon_service == NULL) {
    LoadDefaultImage(request_id);
    return;
  }

  GURL favicon_url =
      AppLaunchInfo::GetFullLaunchURL(GetData(request_id)->extension.get());
  favicon_service->GetRawFaviconForPageURL(
      favicon_url, {favicon_base::IconType::kFavicon}, gfx::kFaviconSize,
      /*fallback_to_host=*/false,
      base::Bind(&ExtensionIconSource::OnFaviconDataAvailable,
                 base::Unretained(this), request_id),
      &cancelable_task_tracker_);
}

void ExtensionIconSource::OnFaviconDataAvailable(
    int request_id,
    const favicon_base::FaviconRawBitmapResult& bitmap_result) {
  ExtensionIconRequest* request = GetData(request_id);

  // Fallback to the default icon if there wasn't a favicon.
  if (!bitmap_result.is_valid()) {
    LoadDefaultImage(request_id);
    return;
  }

  if (!request->grayscale) {
    // If we don't need a grayscale image, then we can bypass FinalizeImage
    // to avoid unnecessary conversions.
    request->callback.Run(bitmap_result.bitmap_data.get());
    ClearData(request_id);
  } else {
    FinalizeImage(ToBitmap(bitmap_result.bitmap_data->front(),
                           bitmap_result.bitmap_data->size()), request_id);
  }
}

void ExtensionIconSource::OnImageLoaded(int request_id,
                                        const gfx::Image& image) {
  if (image.IsEmpty())
    LoadIconFailed(request_id);
  else
    FinalizeImage(image.ToSkBitmap(), request_id);
}

void ExtensionIconSource::LoadIconFailed(int request_id) {
  ExtensionIconRequest* request = GetData(request_id);
  ExtensionResource icon = IconsInfo::GetIconResource(
      request->extension.get(), request->size, request->match);

  if (request->size == extension_misc::EXTENSION_ICON_BITTY)
    LoadFaviconImage(request_id);
  else
    LoadDefaultImage(request_id);
}

bool ExtensionIconSource::ParseData(
    const std::string& path,
    int request_id,
    const content::URLDataSource::GotDataCallback& callback) {
  // Extract the parameters from the path by lower casing and splitting.
  std::string path_lower = base::ToLowerASCII(path);
  std::vector<std::string> path_parts = base::SplitString(
      path_lower, "/", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
  if (path_lower.empty() || path_parts.size() < 3)
    return false;

  std::string size_param = path_parts.at(1);
  std::string match_param = path_parts.at(2);
  match_param = match_param.substr(0, match_param.find('?'));

  int size;
  if (!base::StringToInt(size_param, &size))
    return false;
  if (size <= 0 || size > extension_misc::EXTENSION_ICON_GIGANTOR)
    return false;

  ExtensionIconSet::MatchType match_type;
  int match_num;
  if (!base::StringToInt(match_param, &match_num))
    return false;
  match_type = static_cast<ExtensionIconSet::MatchType>(match_num);
  if (!(match_type == ExtensionIconSet::MATCH_EXACTLY ||
        match_type == ExtensionIconSet::MATCH_SMALLER ||
        match_type == ExtensionIconSet::MATCH_BIGGER))
    match_type = ExtensionIconSet::MATCH_EXACTLY;

  std::string extension_id = path_parts.at(0);
  const Extension* extension = ExtensionSystem::Get(profile_)->
      extension_service()->GetInstalledExtension(extension_id);
  if (!extension)
    return false;

  bool grayscale = path_lower.find("grayscale=true") != std::string::npos;

  SetData(request_id, callback, extension, grayscale, size, match_type);

  return true;
}

void ExtensionIconSource::SetData(
    int request_id,
    const content::URLDataSource::GotDataCallback& callback,
    const Extension* extension,
    bool grayscale,
    int size,
    ExtensionIconSet::MatchType match) {
  std::unique_ptr<ExtensionIconRequest> request =
      std::make_unique<ExtensionIconRequest>();
  request->callback = callback;
  request->extension = extension;
  request->grayscale = grayscale;
  request->size = size;
  request->match = match;
  request_map_[request_id] = std::move(request);
}

ExtensionIconSource::ExtensionIconRequest* ExtensionIconSource::GetData(
    int request_id) {
  return request_map_[request_id].get();
}

void ExtensionIconSource::ClearData(int request_id) {
  request_map_.erase(request_id);
}

}  // namespace extensions
