// Copyright 2014 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/extensions/bookmark_app_helper.h"

#include <cctype>

#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/extensions/crx_installer.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/favicon_downloader.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/image_loader.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/common/url_pattern.h"
#include "grit/platform_locale_settings.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "skia/ext/image_operations.h"
#include "skia/ext/platform_canvas.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_analysis.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/font.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_family.h"
#include "ui/gfx/rect.h"

namespace {

// Overlays a shortcut icon over the bottom left corner of a given image.
class GeneratedIconImageSource : public gfx::CanvasImageSource {
 public:
  explicit GeneratedIconImageSource(char letter, SkColor color, int output_size)
      : gfx::CanvasImageSource(gfx::Size(output_size, output_size), false),
        letter_(letter),
        color_(color),
        output_size_(output_size) {}
  virtual ~GeneratedIconImageSource() {}

 private:
  // gfx::CanvasImageSource overrides:
  virtual void Draw(gfx::Canvas* canvas) OVERRIDE {
    const unsigned char kLuminanceThreshold = 190;
    const int icon_size = output_size_ * 3 / 4;
    const int icon_inset = output_size_ / 8;
    const size_t border_radius = output_size_ / 16;
    const size_t font_size = output_size_ * 7 / 16;

    std::string font_name =
        l10n_util::GetStringUTF8(IDS_SANS_SERIF_FONT_FAMILY);
#if defined(OS_CHROMEOS)
    const std::string kChromeOSFontFamily = "Noto Sans";
    font_name = kChromeOSFontFamily;
#endif

    // Draw a rounded rect of the given |color|.
    SkPaint background_paint;
    background_paint.setFlags(SkPaint::kAntiAlias_Flag);
    background_paint.setColor(color_);

    gfx::Rect icon_rect(icon_inset, icon_inset, icon_size, icon_size);
    canvas->DrawRoundRect(icon_rect, border_radius, background_paint);

    // The text rect's size needs to be odd to center the text correctly.
    gfx::Rect text_rect(icon_inset, icon_inset, icon_size + 1, icon_size + 1);
    // Draw the letter onto the rounded rect. The letter's color depends on the
    // luminance of |color|.
    unsigned char luminance = color_utils::GetLuminanceForColor(color_);
    canvas->DrawStringRectWithFlags(
        base::string16(1, std::toupper(letter_)),
        gfx::FontList(gfx::Font(font_name, font_size)),
        luminance > kLuminanceThreshold ? SK_ColorBLACK : SK_ColorWHITE,
        text_rect,
        gfx::Canvas::TEXT_ALIGN_CENTER);
  }

  char letter_;

  SkColor color_;

  int output_size_;

  DISALLOW_COPY_AND_ASSIGN(GeneratedIconImageSource);
};

void OnIconsLoaded(
    WebApplicationInfo web_app_info,
    const base::Callback<void(const WebApplicationInfo&)> callback,
    const gfx::ImageFamily& image_family) {
  for (gfx::ImageFamily::const_iterator it = image_family.begin();
       it != image_family.end();
       ++it) {
    WebApplicationInfo::IconInfo icon_info;
    icon_info.data = *it->ToSkBitmap();
    icon_info.width = icon_info.data.width();
    icon_info.height = icon_info.data.height();
    web_app_info.icons.push_back(icon_info);
  }
  callback.Run(web_app_info);
}

}  // namespace

namespace extensions {

// static
std::map<int, SkBitmap> BookmarkAppHelper::ConstrainBitmapsToSizes(
    const std::vector<SkBitmap>& bitmaps,
    const std::set<int>& sizes) {
  std::map<int, SkBitmap> output_bitmaps;
  std::map<int, SkBitmap> ordered_bitmaps;
  for (std::vector<SkBitmap>::const_iterator it = bitmaps.begin();
       it != bitmaps.end();
       ++it) {
    DCHECK(it->width() == it->height());
    ordered_bitmaps[it->width()] = *it;
  }

  std::set<int>::const_iterator sizes_it = sizes.begin();
  std::map<int, SkBitmap>::const_iterator bitmaps_it = ordered_bitmaps.begin();
  while (sizes_it != sizes.end() && bitmaps_it != ordered_bitmaps.end()) {
    int size = *sizes_it;
    // Find the closest not-smaller bitmap.
    bitmaps_it = ordered_bitmaps.lower_bound(size);
    ++sizes_it;
    // Ensure the bitmap is valid and smaller than the next allowed size.
    if (bitmaps_it != ordered_bitmaps.end() &&
        (sizes_it == sizes.end() || bitmaps_it->second.width() < *sizes_it)) {
      // Resize the bitmap if it does not exactly match the desired size.
      output_bitmaps[size] = bitmaps_it->second.width() == size
                                 ? bitmaps_it->second
                                 : skia::ImageOperations::Resize(
                                       bitmaps_it->second,
                                       skia::ImageOperations::RESIZE_LANCZOS3,
                                       size,
                                       size);
    }
  }
  return output_bitmaps;
}

// static
void BookmarkAppHelper::GenerateIcon(std::map<int, SkBitmap>* bitmaps,
                                     int output_size,
                                     SkColor color,
                                     char letter) {
  // Do nothing if there is already an icon of |output_size|.
  if (bitmaps->count(output_size))
    return;

  gfx::ImageSkia icon_image(
      new GeneratedIconImageSource(letter, color, output_size),
      gfx::Size(output_size, output_size));
  icon_image.bitmap()->deepCopyTo(&(*bitmaps)[output_size]);
}

BookmarkAppHelper::BookmarkAppHelper(ExtensionService* service,
                                     WebApplicationInfo web_app_info,
                                     content::WebContents* contents)
    : web_app_info_(web_app_info),
      crx_installer_(extensions::CrxInstaller::CreateSilent(service)) {
  registrar_.Add(this,
                 chrome::NOTIFICATION_CRX_INSTALLER_DONE,
                 content::Source<CrxInstaller>(crx_installer_.get()));

  registrar_.Add(this,
                 chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR,
                 content::Source<CrxInstaller>(crx_installer_.get()));

  crx_installer_->set_error_on_unsupported_requirements(true);

  if (!contents)
    return;

  // Add urls from the WebApplicationInfo.
  std::vector<GURL> web_app_info_icon_urls;
  for (std::vector<WebApplicationInfo::IconInfo>::const_iterator it =
           web_app_info_.icons.begin();
       it != web_app_info_.icons.end();
       ++it) {
    if (it->url.is_valid())
      web_app_info_icon_urls.push_back(it->url);
  }

  favicon_downloader_.reset(
      new FaviconDownloader(contents,
                            web_app_info_icon_urls,
                            base::Bind(&BookmarkAppHelper::OnIconsDownloaded,
                                       base::Unretained(this))));
}

BookmarkAppHelper::~BookmarkAppHelper() {}

void BookmarkAppHelper::Create(const CreateBookmarkAppCallback& callback) {
  callback_ = callback;

  if (favicon_downloader_.get())
    favicon_downloader_->Start();
  else
    OnIconsDownloaded(true, std::map<GURL, std::vector<SkBitmap> >());
}

void BookmarkAppHelper::OnIconsDownloaded(
    bool success,
    const std::map<GURL, std::vector<SkBitmap> >& bitmaps) {
  // The tab has navigated away during the icon download. Cancel the bookmark
  // app creation.
  if (!success) {
    favicon_downloader_.reset();
    callback_.Run(NULL, web_app_info_);
    return;
  }

  // Add the downloaded icons. Extensions only allow certain icon sizes. First
  // populate icons that match the allowed sizes exactly and then downscale
  // remaining icons to the closest allowed size that doesn't yet have an icon.
  std::set<int> allowed_sizes(extension_misc::kExtensionIconSizes,
                              extension_misc::kExtensionIconSizes +
                                  extension_misc::kNumExtensionIconSizes);
  std::vector<SkBitmap> downloaded_icons;
  for (FaviconDownloader::FaviconMap::const_iterator map_it = bitmaps.begin();
       map_it != bitmaps.end();
       ++map_it) {
    for (std::vector<SkBitmap>::const_iterator bitmap_it =
             map_it->second.begin();
         bitmap_it != map_it->second.end();
         ++bitmap_it) {
      if (bitmap_it->empty() || bitmap_it->width() != bitmap_it->height())
        continue;

      downloaded_icons.push_back(*bitmap_it);
    }
  }

  // Add all existing icons from WebApplicationInfo.
  for (std::vector<WebApplicationInfo::IconInfo>::const_iterator it =
           web_app_info_.icons.begin();
       it != web_app_info_.icons.end();
       ++it) {
    const SkBitmap& icon = it->data;
    if (!icon.drawsNothing() && icon.width() == icon.height())
      downloaded_icons.push_back(icon);
  }

  web_app_info_.icons.clear();

  // If there are icons that don't match the accepted icon sizes, find the
  // closest bigger icon to the accepted sizes and resize the icon to it. An
  // icon will be resized and used for at most one size.
  std::map<int, SkBitmap> resized_bitmaps(
      ConstrainBitmapsToSizes(downloaded_icons, allowed_sizes));

  // Generate container icons from smaller icons.
  const int kIconSizesToGenerate[] = {extension_misc::EXTENSION_ICON_SMALL,
                                      extension_misc::EXTENSION_ICON_MEDIUM, };
  const std::set<int> generate_sizes(
      kIconSizesToGenerate,
      kIconSizesToGenerate + arraysize(kIconSizesToGenerate));

  // Only generate icons if larger icons don't exist. This means the app
  // launcher and the taskbar will do their best downsizing large icons and
  // these icons are only generated as a last resort against upscaling a smaller
  // icon.
  if (resized_bitmaps.lower_bound(*generate_sizes.rbegin()) ==
      resized_bitmaps.end()) {
    GURL app_url = web_app_info_.app_url;

    // The letter that will be painted on the generated icon.
    char icon_letter = ' ';
    std::string domain_and_registry(
        net::registry_controlled_domains::GetDomainAndRegistry(
            app_url,
            net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES));
    if (!domain_and_registry.empty()) {
      icon_letter = domain_and_registry[0];
    } else if (!app_url.host().empty()) {
      icon_letter = app_url.host()[0];
    }

    // The color that will be used for the icon's background.
    SkColor background_color = SK_ColorBLACK;
    if (resized_bitmaps.size()) {
      color_utils::GridSampler sampler;
      background_color = color_utils::CalculateKMeanColorOfBitmap(
          resized_bitmaps.begin()->second);
    }

    for (std::set<int>::const_iterator it = generate_sizes.begin();
         it != generate_sizes.end();
         ++it) {
      GenerateIcon(&resized_bitmaps, *it, background_color, icon_letter);
      // Also generate the 2x resource for this size.
      GenerateIcon(&resized_bitmaps, *it * 2, background_color, icon_letter);
    }
  }

  // Populate the icon data into the WebApplicationInfo we are using to
  // install the bookmark app.
  for (std::map<int, SkBitmap>::const_iterator resized_bitmaps_it =
           resized_bitmaps.begin();
       resized_bitmaps_it != resized_bitmaps.end();
       ++resized_bitmaps_it) {
    WebApplicationInfo::IconInfo icon_info;
    icon_info.data = resized_bitmaps_it->second;
    icon_info.width = icon_info.data.width();
    icon_info.height = icon_info.data.height();
    web_app_info_.icons.push_back(icon_info);
  }

  // Install the app.
  crx_installer_->InstallWebApp(web_app_info_);
  favicon_downloader_.reset();
}

void BookmarkAppHelper::Observe(int type,
                                const content::NotificationSource& source,
                                const content::NotificationDetails& details) {
  switch (type) {
    case chrome::NOTIFICATION_CRX_INSTALLER_DONE: {
      const Extension* extension =
          content::Details<const Extension>(details).ptr();
      DCHECK(extension);
      DCHECK_EQ(AppLaunchInfo::GetLaunchWebURL(extension),
                web_app_info_.app_url);
      callback_.Run(extension, web_app_info_);
      break;
    }
    case chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR:
      callback_.Run(NULL, web_app_info_);
      break;
    default:
      NOTREACHED();
      break;
  }
}

void CreateOrUpdateBookmarkApp(ExtensionService* service,
                               WebApplicationInfo& web_app_info) {
  scoped_refptr<extensions::CrxInstaller> installer(
      extensions::CrxInstaller::CreateSilent(service));
  installer->set_error_on_unsupported_requirements(true);
  installer->InstallWebApp(web_app_info);
}

void GetWebApplicationInfoFromApp(
    content::BrowserContext* browser_context,
    const extensions::Extension* extension,
    const base::Callback<void(const WebApplicationInfo&)> callback) {
  if (!extension->from_bookmark()) {
    callback.Run(WebApplicationInfo());
    return;
  }

  WebApplicationInfo web_app_info;
  web_app_info.app_url = AppLaunchInfo::GetLaunchWebURL(extension);
  web_app_info.title = base::UTF8ToUTF16(extension->non_localized_name());
  web_app_info.description = base::UTF8ToUTF16(extension->description());

  std::vector<extensions::ImageLoader::ImageRepresentation> info_list;
  for (size_t i = 0; i < extension_misc::kNumExtensionIconSizes; ++i) {
    int size = extension_misc::kExtensionIconSizes[i];
    extensions::ExtensionResource resource =
        extensions::IconsInfo::GetIconResource(
            extension, size, ExtensionIconSet::MATCH_EXACTLY);
    if (!resource.empty()) {
      info_list.push_back(extensions::ImageLoader::ImageRepresentation(
          resource,
          extensions::ImageLoader::ImageRepresentation::ALWAYS_RESIZE,
          gfx::Size(size, size),
          ui::SCALE_FACTOR_100P));
    }
  }

  extensions::ImageLoader::Get(browser_context)->LoadImageFamilyAsync(
      extension, info_list, base::Bind(&OnIconsLoaded, web_app_info, callback));
}

bool IsValidBookmarkAppUrl(const GURL& url) {
  URLPattern origin_only_pattern(Extension::kValidWebExtentSchemes);
  origin_only_pattern.SetMatchAllURLs(true);
  return url.is_valid() && origin_only_pattern.MatchesURL(url);
}

}  // namespace extensions