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

#include "extensions/browser/extension_action.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "base/check_op.h"
#include "base/strings/string_number_conversions.h"
#include "extensions/browser/extension_icon_image.h"
#include "extensions/browser/extension_icon_placeholder.h"
#include "extensions/common/constants.h"
#include "extensions/common/icons/extension_icon_set.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/grit/extensions_browser_resources.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/effects/SkGradient.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/animation/animation_delegate.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/gfx/image/image_skia_source.h"
#include "ui/gfx/skbitmap_operations.h"
#include "url/gurl.h"

namespace extensions {

namespace {

class GetAttentionImageSource : public gfx::ImageSkiaSource {
 public:
  explicit GetAttentionImageSource(const gfx::ImageSkia& icon) : icon_(icon) {}

  // gfx::ImageSkiaSource overrides:
  gfx::ImageSkiaRep GetImageForScale(float scale) override {
    gfx::ImageSkiaRep icon_rep = icon_.GetRepresentation(scale);
    color_utils::HSL shift = {-1, 0, 0.5};
    return gfx::ImageSkiaRep(
        SkBitmapOperations::CreateHSLShiftedBitmap(icon_rep.GetBitmap(), shift),
        icon_rep.scale());
  }

 private:
  const gfx::ImageSkia icon_;
};

struct IconRepresentationInfo {
  // Size as a string that will be used to retrieve a representation value from
  // SetIcon function arguments.
  const char* size_string;
  // Scale factor for which the representation should be used.
  ui::ResourceScaleFactor scale;
};

template <class T>
bool HasValue(const std::map<int, T>& map, ExtensionAction::TabID tab_id) {
  return map.contains(tab_id);
}

}  // namespace

// static
// LINT.IfChange(ActionIconSize)
extension_misc::ExtensionIcons ExtensionAction::ActionIconSize() {
#if BUILDFLAG(IS_ANDROID)
  return extension_misc::EXTENSION_ICON_MINI;
#else
  return extension_misc::EXTENSION_ICON_BITTY;
#endif
}
// LINT.ThenChange(/extensions/browser/icon_util.cc:ActionIconSize)

// static
gfx::Image ExtensionAction::FallbackIcon() {
  return ui::ResourceBundle::GetSharedInstance().GetImageNamed(
      IDR_EXTENSIONS_FAVICON);
}

const int ExtensionAction::kDefaultTabId = -1;

ExtensionAction::ExtensionAction(const Extension& extension,
                                 const ActionInfo& manifest_data)
    : extension_id_(extension.id()),
      extension_name_(extension.name()),
      action_type_(manifest_data.type),
      default_state_(manifest_data.default_state) {
  SetIsVisible(kDefaultTabId,
               default_state_ == ActionInfo::DefaultState::kEnabled);
  Populate(extension, manifest_data);
}

ExtensionAction::~ExtensionAction() = default;

void ExtensionAction::SetPopupUrl(TabID tab_id, const GURL& url) {
  // We store |url| even if it is empty, rather than removing a URL from the
  // map.  If an extension has a default popup, and removes it for a tab via
  // the API, we must remember that there is no popup for that specific tab.
  // If we removed the tab's URL, GetPopupURL would incorrectly return the
  // default URL.
  SetValue(&popup_url_, tab_id, url);
}

bool ExtensionAction::HasPopup(TabID tab_id) const {
  return !GetPopupUrl(tab_id).is_empty();
}

GURL ExtensionAction::GetPopupUrl(TabID tab_id) const {
  return GetValue(popup_url_, tab_id);
}

void ExtensionAction::SetIcon(TabID tab_id, const gfx::Image& image) {
  SetValue(&icon_, tab_id, image);
}

gfx::Image ExtensionAction::GetExplicitlySetIcon(TabID tab_id) const {
  return GetValue(icon_, tab_id);
}

bool ExtensionAction::SetIsVisible(TabID tab_id, bool new_visibility) {
  const bool old_visibility = GetValue(is_visible_, tab_id);

  if (old_visibility == new_visibility) {
    return false;
  }

  SetValue(&is_visible_, tab_id, new_visibility);

  return true;
}

void ExtensionAction::DeclarativeShow(TabID tab_id) {
  DCHECK_NE(tab_id, kDefaultTabId);
  ++declarative_show_count_[tab_id];  // Use default initialization to 0.
}

void ExtensionAction::UndoDeclarativeShow(TabID tab_id) {
  int& show_count = declarative_show_count_[tab_id];
  DCHECK_GT(show_count, 0);
  if (--show_count == 0) {
    declarative_show_count_.erase(tab_id);
  }
}

void ExtensionAction::DeclarativeSetIcon(TabID tab_id,
                                         int priority,
                                         const gfx::Image& icon) {
  DCHECK_NE(tab_id, kDefaultTabId);
  declarative_icon_[tab_id][priority].push_back(icon);
}

void ExtensionAction::UndoDeclarativeSetIcon(TabID tab_id,
                                             int priority,
                                             const gfx::Image& icon) {
  std::vector<gfx::Image>& icons = declarative_icon_[tab_id][priority];
  for (auto it = icons.begin(); it != icons.end(); ++it) {
    if (it->AsImageSkia().BackedBySameObjectAs(icon.AsImageSkia())) {
      icons.erase(it);
      return;
    }
  }
}

const gfx::Image ExtensionAction::GetDeclarativeIcon(TabID tab_id) const {
  auto it = declarative_icon_.find(tab_id);
  if (it != declarative_icon_.end() && !it->second.rbegin()->second.empty()) {
    return it->second.rbegin()->second.back();
  }
  return gfx::Image();
}

void ExtensionAction::ClearAllValuesForTab(TabID tab_id) {
  popup_url_.erase(tab_id);
  title_.erase(tab_id);
  icon_.erase(tab_id);
  badge_text_.erase(tab_id);
  dnr_action_count_.erase(tab_id);
  badge_text_color_.erase(tab_id);
  badge_background_color_.erase(tab_id);
  is_visible_.erase(tab_id);
}

void ExtensionAction::ClearDeclarativeValuesForTab(TabID tab_id) {
  declarative_show_count_.erase(tab_id);
  declarative_icon_.erase(tab_id);
}

void ExtensionAction::SetDefaultIconImage(
    std::unique_ptr<IconImage> icon_image) {
  default_icon_image_ = std::move(icon_image);
}

gfx::Image ExtensionAction::GetDefaultIconImage() const {
  // If we have a default icon, it should be loaded before trying to use it.
  DCHECK(!default_icon_image_ == !default_icon_);
  if (default_icon_image_) {
    return default_icon_image_->image();
  }

  return GetPlaceholderIconImage();
}

gfx::Image ExtensionAction::GetPlaceholderIconImage() const {
  if (placeholder_icon_image_.IsEmpty()) {
    // For extension actions, we use a special placeholder icon (with the first
    // letter of the extension name) rather than the default (puzzle piece).
    // Note that this is only if we can't find any better image (e.g. a product
    // icon).
    placeholder_icon_image_ = ExtensionIconPlaceholder::CreateImage(
        ActionIconSize(), extension_name_);
  }

  return placeholder_icon_image_;
}

std::string ExtensionAction::GetDisplayBadgeText(TabID tab_id) const {
  // Tab specific badge text set by an extension overrides the automatically set
  // action count. Action count should only be shown if at least one action is
  // matched.
  bool use_dnr_action_count =
      !HasBadgeText(tab_id) && GetDNRActionCount(tab_id) > 0;
  return use_dnr_action_count ? base::NumberToString(GetDNRActionCount(tab_id))
                              : GetExplicitlySetBadgeText(tab_id);
}

bool ExtensionAction::HasPopupUrl(TabID tab_id) const {
  return HasValue(popup_url_, tab_id);
}

bool ExtensionAction::HasTitle(TabID tab_id) const {
  return HasValue(title_, tab_id);
}

bool ExtensionAction::HasBadgeText(TabID tab_id) const {
  return HasValue(badge_text_, tab_id);
}

bool ExtensionAction::HasBadgeBackgroundColor(TabID tab_id) const {
  return HasValue(badge_background_color_, tab_id);
}

bool ExtensionAction::HasBadgeTextColor(TabID tab_id) const {
  return HasValue(badge_text_color_, tab_id);
}

bool ExtensionAction::HasIsVisible(TabID tab_id) const {
  return HasValue(is_visible_, tab_id);
}

bool ExtensionAction::HasIcon(TabID tab_id) const {
  return HasValue(icon_, tab_id);
}

bool ExtensionAction::HasDNRActionCount(TabID tab_id) const {
  return HasValue(dnr_action_count_, tab_id);
}

void ExtensionAction::SetDefaultIconForTest(
    std::unique_ptr<ExtensionIconSet> default_icon) {
  default_icon_ = std::move(default_icon);
}

void ExtensionAction::Populate(const Extension& extension,
                               const ActionInfo& manifest_data) {
  // If the manifest doesn't specify a title, set it to |extension|'s name.
  const std::string& title = !manifest_data.default_title.empty()
                                 ? manifest_data.default_title
                                 : extension.name();
  SetTitle(kDefaultTabId, title);
  SetPopupUrl(kDefaultTabId, manifest_data.default_popup_url);

  // Initialize the specified icon set.
  if (!manifest_data.default_icon.empty()) {
    default_icon_ =
        std::make_unique<ExtensionIconSet>(manifest_data.default_icon);
  } else {
    // Fall back to the product icons if no action icon exists.
    const ExtensionIconSet& product_icons = IconsInfo::GetIcons(&extension);
    if (!product_icons.empty()) {
      default_icon_ = std::make_unique<ExtensionIconSet>(product_icons);
    }
  }
}

// Determines which icon would be returned by |GetIcon|, and returns its width.
int ExtensionAction::GetIconWidth(TabID tab_id) const {
  // If icon has been set, return its width.
  gfx::Image icon = GetValue(icon_, tab_id);
  if (!icon.IsEmpty()) {
    return icon.Width();
  }
  // If there is a default icon, the icon width will be set depending on our
  // action type.
  if (default_icon_) {
    return ActionIconSize();
  }

  // If no icon has been set and there is no default icon, we need favicon
  // width.
  return FallbackIcon().Width();
}

bool ExtensionAction::GetIsVisibleInternal(TabID tab_id,
                                           bool include_declarative) const {
  if (const bool* tab_is_visible = base::FindOrNull(is_visible_, tab_id)) {
    return *tab_is_visible;
  }

  if (include_declarative && declarative_show_count_.contains(tab_id)) {
    return true;
  }

  if (const bool* default_is_visible =
          base::FindOrNull(is_visible_, kDefaultTabId)) {
    return *default_is_visible;
  }

  return false;
}

}  // namespace extensions
