// 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/theme_source.h"

#include "base/memory/ref_counted_memory.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/resources_util.h"
#include "chrome/browser/search/instant_io_context.h"
#include "chrome/browser/themes/browser_theme_pack.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/webui/ntp/ntp_resource_cache.h"
#include "chrome/browser/ui/webui/ntp/ntp_resource_cache_factory.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/theme_resources.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_thread.h"
#include "net/url_request/url_request.h"
#include "ui/base/layout.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "url/gurl.h"

namespace {

GURL GetThemeUrl(const std::string& path) {
  return GURL(std::string(content::kChromeUIScheme) + "://" +
              std::string(chrome::kChromeUIThemeHost) + "/" + path);
}

bool IsNewTabCssPath(const std::string& path) {
  static const char kNewTabCSSPath[] = "css/new_tab_theme.css";
  static const char kIncognitoNewTabCSSPath[] =
      "css/incognito_new_tab_theme.css";
  return (path == kNewTabCSSPath) || (path == kIncognitoNewTabCSSPath);
}

void ProcessImageOnUiThread(const gfx::ImageSkia& image,
                            float scale,
                            scoped_refptr<base::RefCountedBytes> data) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  const gfx::ImageSkiaRep& rep = image.GetRepresentation(scale);
  gfx::PNGCodec::EncodeBGRASkBitmap(
      rep.sk_bitmap(), false /* discard transparency */, &data->data());
}

void ProcessResourceOnUiThread(int resource_id,
                               float scale,
                               scoped_refptr<base::RefCountedBytes> data) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  ProcessImageOnUiThread(
      *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(resource_id),
      scale, data);
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// ThemeSource, public:

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

ThemeSource::~ThemeSource() = default;

std::string ThemeSource::GetSource() const {
  return chrome::kChromeUIThemeHost;
}

void ThemeSource::StartDataRequest(
    const std::string& path,
    const content::ResourceRequestInfo::WebContentsGetter& wc_getter,
    const content::URLDataSource::GotDataCallback& callback) {
  // Default scale factor if not specified.
  float scale = 1.0f;
  // All frames by default if not specified.
  int frame = -1;
  std::string parsed_path;
  webui::ParsePathAndImageSpec(GetThemeUrl(path), &parsed_path, &scale, &frame);

  if (IsNewTabCssPath(parsed_path)) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    NTPResourceCache::WindowType type =
        NTPResourceCache::GetWindowType(profile_, /*render_host=*/nullptr);
    NTPResourceCache* cache = NTPResourceCacheFactory::GetForProfile(profile_);
    callback.Run(cache->GetNewTabCSS(type));
    return;
  }

  int resource_id = -1;
  if (parsed_path == "current-channel-logo") {
    switch (chrome::GetChannel()) {
#if defined(GOOGLE_CHROME_BUILD)
      case version_info::Channel::CANARY:
        resource_id = IDR_PRODUCT_LOGO_32_CANARY;
        break;
      case version_info::Channel::DEV:
        resource_id = IDR_PRODUCT_LOGO_32_DEV;
        break;
      case version_info::Channel::BETA:
        resource_id = IDR_PRODUCT_LOGO_32_BETA;
        break;
      case version_info::Channel::STABLE:
        resource_id = IDR_PRODUCT_LOGO_32;
        break;
#else
      case version_info::Channel::CANARY:
      case version_info::Channel::DEV:
      case version_info::Channel::BETA:
      case version_info::Channel::STABLE:
        NOTREACHED();
        FALLTHROUGH;
#endif
      case version_info::Channel::UNKNOWN:
        resource_id = IDR_PRODUCT_LOGO_32;
        break;
    }
  } else {
    resource_id = ResourcesUtil::GetThemeResourceId(parsed_path);
  }

  // Limit the maximum scale we'll respond to.  Very large scale factors can
  // take significant time to serve or, at worst, crash the browser due to OOM.
  // We don't want to clamp to the max scale factor, though, for devices that
  // use 2x scale without 2x data packs, as well as omnibox requests for larger
  // (but still reasonable) scales (see below).
  const float max_scale = ui::GetScaleForScaleFactor(
      ui::ResourceBundle::GetSharedInstance().GetMaxScaleFactor());
  const float unreasonable_scale = max_scale * 32;
  // TODO(reveman): Add support frames beyond 0 (crbug.com/750064).
  if ((resource_id == -1) || (scale >= unreasonable_scale) || (frame > 0)) {
    // Either we have no data to send back, or the requested scale is
    // unreasonably large.  This shouldn't happen normally, as chrome://theme/
    // URLs are only used by WebUI pages and component extensions.  However, the
    // user can also enter these into the omnibox, so we need to fail
    // gracefully.
    callback.Run(nullptr);
  } else if ((GetMimeType(path) == "image/png") &&
             ((scale > max_scale) || (frame != -1))) {
    // This will extract and scale frame 0 of animated images.
    // TODO(reveman): Support scaling of animated images and avoid scaling and
    // re-encode when specific frame is specified (crbug.com/750064).
    DCHECK_LE(frame, 0);
    SendThemeImage(callback, resource_id, scale);
  } else {
    SendThemeBitmap(callback, resource_id, scale);
  }
}

std::string ThemeSource::GetMimeType(const std::string& path) const {
  std::string parsed_path;
  webui::ParsePathAndScale(GetThemeUrl(path), &parsed_path, nullptr);
  return IsNewTabCssPath(parsed_path) ? "text/css" : "image/png";
}

scoped_refptr<base::SingleThreadTaskRunner>
ThemeSource::TaskRunnerForRequestPath(const std::string& path) const {
  std::string parsed_path;
  webui::ParsePathAndScale(GetThemeUrl(path), &parsed_path, nullptr);

  if (IsNewTabCssPath(parsed_path)) {
    // We'll get this data from the NTPResourceCache, which must be accessed on
    // the UI thread.
    return content::URLDataSource::TaskRunnerForRequestPath(path);
  }

  // If it's not a themeable image, we don't need to go to the UI thread.
  int resource_id = ResourcesUtil::GetThemeResourceId(parsed_path);
  return BrowserThemePack::IsPersistentImageID(resource_id)
             ? content::URLDataSource::TaskRunnerForRequestPath(path)
             : nullptr;
}

bool ThemeSource::AllowCaching() const {
  return false;
}

bool ThemeSource::ShouldServiceRequest(
    const GURL& url,
    content::ResourceContext* resource_context,
    int render_process_id) const {
  return url.SchemeIs(chrome::kChromeSearchScheme)
             ? InstantIOContext::ShouldServiceRequest(url, resource_context,
                                                      render_process_id)
             : URLDataSource::ShouldServiceRequest(url, resource_context,
                                                   render_process_id);
}

////////////////////////////////////////////////////////////////////////////////
// ThemeSource, private:

void ThemeSource::SendThemeBitmap(
    const content::URLDataSource::GotDataCallback& callback,
    int resource_id,
    float scale) {
  ui::ScaleFactor scale_factor = ui::GetSupportedScaleFactor(scale);
  if (BrowserThemePack::IsPersistentImageID(resource_id)) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    scoped_refptr<base::RefCountedMemory> image_data(
        ThemeService::GetThemeProviderForProfile(profile_->GetOriginalProfile())
            .GetRawData(resource_id, scale_factor));
    callback.Run(image_data.get());
  } else {
    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
    const ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    callback.Run(rb.LoadDataResourceBytesForScale(resource_id, scale_factor));
  }
}

void ThemeSource::SendThemeImage(
    const content::URLDataSource::GotDataCallback& callback,
    int resource_id,
    float scale) {
  scoped_refptr<base::RefCountedBytes> data(new base::RefCountedBytes());
  if (BrowserThemePack::IsPersistentImageID(resource_id)) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    const ui::ThemeProvider& tp = ThemeService::GetThemeProviderForProfile(
        profile_->GetOriginalProfile());
    ProcessImageOnUiThread(*tp.GetImageSkiaNamed(resource_id), scale, data);
    callback.Run(data.get());
  } else {
    DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
    // Fetching image data in ResourceBundle should happen on the UI thread. See
    // crbug.com/449277
    content::BrowserThread::PostTaskAndReply(
        content::BrowserThread::UI, FROM_HERE,
        base::BindOnce(&ProcessResourceOnUiThread, resource_id, scale, data),
        base::BindOnce(callback, data));
  }
}
