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

#include "chrome/browser/extensions/extension_action_storage_manager.h"

#include <stdint.h>

#include <memory>
#include <utility>

#include "base/base64.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "extensions/browser/extension_action_manager.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/state_store.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_id.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

const char kBrowserActionStorageKey[] = "browser_action";
const char kPopupUrlStorageKey[] = "poupup_url";
const char kTitleStorageKey[] = "title";
const char kIconStorageKey[] = "icon";
const char kBadgeTextStorageKey[] = "badge_text";
const char kBadgeBackgroundColorStorageKey[] = "badge_background_color";
const char kBadgeTextColorStorageKey[] = "badge_text_color";
const char kAppearanceStorageKey[] = "appearance";

// Only add values to the end of this enum, since it's stored in the user's
// Extension State, under the kAppearanceStorageKey.  It represents the
// ExtensionAction's default visibility.
enum StoredAppearance {
  // The action icon is hidden.
  INVISIBLE = 0,
  // The action is trying to get the user's attention but isn't yet
  // running on the page.  Was only used for script badges.
  OBSOLETE_WANTS_ATTENTION = 1,
  // The action icon is visible with its normal appearance.
  ACTIVE = 2,
};

// Conversion function for reading/writing to storage.
SkColor RawStringToSkColor(const std::string& str) {
  uint64_t value = 0;
  base::StringToUint64(str, &value);
  SkColor color = static_cast<SkColor>(value);
  DCHECK(value == color);  // ensure value fits into color's 32 bits
  return color;
}

// Conversion function for reading/writing to storage.
std::string SkColorToRawString(SkColor color) {
  return base::NumberToString(color);
}

// Conversion function for reading/writing to storage.
SkBitmap StringToSkBitmap(const std::string& str) {
  std::optional<std::vector<uint8_t>> decoded = base::Base64Decode(str);
  if (!decoded) {
    return SkBitmap();
  }

  return gfx::PNGCodec::Decode(decoded.value());
}

// Conversion function for reading/writing to storage.
std::string BitmapToString(const SkBitmap& bitmap) {
  std::optional<std::vector<uint8_t>> data =
      gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, /*discard_transparency=*/false);
  if (!data) {
    return std::string();
  }

  return base::Base64Encode(data.value());
}

// Set |action|'s default values to those specified in |dict|.
void SetDefaultsFromValue(const base::DictValue& dict,
                          ExtensionAction* action) {
  const int kDefaultTabId = ExtensionAction::kDefaultTabId;

  // For each value, don't set it if it has been modified already.
  const std::string* popup_url = dict.FindString(kPopupUrlStorageKey);
  if (popup_url && !action->HasPopupUrl(kDefaultTabId)) {
    action->SetPopupUrl(kDefaultTabId, GURL(*popup_url));
  }
  const std::string* title = dict.FindString(kTitleStorageKey);
  if (title && !action->HasTitle(kDefaultTabId)) {
    action->SetTitle(kDefaultTabId, *title);
  }
  const std::string* badge_text = dict.FindString(kBadgeTextStorageKey);
  if (badge_text && !action->HasBadgeText(kDefaultTabId)) {
    action->SetBadgeText(kDefaultTabId, *badge_text);
  }
  const std::string* badge_background_color =
      dict.FindString(kBadgeBackgroundColorStorageKey);
  if (badge_background_color &&
      !action->HasBadgeBackgroundColor(kDefaultTabId)) {
    action->SetBadgeBackgroundColor(
        kDefaultTabId, RawStringToSkColor(*badge_background_color));
  }
  const std::string* badge_text_color =
      dict.FindString(kBadgeTextColorStorageKey);
  if (badge_text_color && !action->HasBadgeTextColor(kDefaultTabId)) {
    action->SetBadgeTextColor(kDefaultTabId,
                              RawStringToSkColor(*badge_text_color));
  }

  std::optional<int> appearance_storage = dict.FindInt(kAppearanceStorageKey);
  if (appearance_storage && !action->HasIsVisible(kDefaultTabId)) {
    switch (*appearance_storage) {
      case INVISIBLE:
      case OBSOLETE_WANTS_ATTENTION:
        action->SetIsVisible(kDefaultTabId, false);
        break;
      case ACTIVE:
        action->SetIsVisible(kDefaultTabId, true);
        break;
    }
  }

  const base::DictValue* icon_dict = dict.FindDict(kIconStorageKey);
  if (icon_dict && !action->HasIcon(kDefaultTabId)) {
    gfx::ImageSkia icon;
    for (const auto iter : *icon_dict) {
      int icon_size = 0;
      if (base::StringToInt(iter.first, &icon_size) &&
          iter.second.is_string()) {
        SkBitmap bitmap = StringToSkBitmap(iter.second.GetString());
        if (!bitmap.isNull()) {
          float scale =
              static_cast<float>(icon_size) / ExtensionAction::ActionIconSize();
          icon.AddRepresentation(gfx::ImageSkiaRep(bitmap, scale));
        }
      }
    }
    action->SetIcon(kDefaultTabId, gfx::Image(icon));
  }
}

// Store |action|'s default values in a base::DictValue for use in storing to
// disk.
base::DictValue DefaultsToValue(ExtensionAction* action) {
  const int kDefaultTabId = ExtensionAction::kDefaultTabId;
  base::DictValue dict;

  dict.Set(kPopupUrlStorageKey, action->GetPopupUrl(kDefaultTabId).spec());
  dict.Set(kTitleStorageKey, action->GetTitle(kDefaultTabId));
  dict.Set(kBadgeTextStorageKey,
           action->GetExplicitlySetBadgeText(kDefaultTabId));
  dict.Set(kBadgeBackgroundColorStorageKey,
           SkColorToRawString(action->GetBadgeBackgroundColor(kDefaultTabId)));
  dict.Set(kBadgeTextColorStorageKey,
           SkColorToRawString(action->GetBadgeTextColor(kDefaultTabId)));
  dict.Set(kAppearanceStorageKey,
           action->GetIsVisible(kDefaultTabId) ? ACTIVE : INVISIBLE);

  gfx::ImageSkia icon =
      action->GetExplicitlySetIcon(kDefaultTabId).AsImageSkia();
  if (!icon.isNull()) {
    base::DictValue icon_value;
    std::vector<gfx::ImageSkiaRep> image_reps = icon.image_reps();
    for (const gfx::ImageSkiaRep& rep : image_reps) {
      int size = static_cast<int>(rep.scale() * icon.width());
      std::string size_string = base::NumberToString(size);
      icon_value.Set(size_string, BitmapToString(rep.GetBitmap()));
    }
    dict.Set(kIconStorageKey, std::move(icon_value));
  }
  return dict;
}

}  // namespace

ExtensionActionStorageManager::ExtensionActionStorageManager(
    content::BrowserContext* context)
    : browser_context_(context) {
  extension_action_dispatcher_observation_.Observe(
      ExtensionActionDispatcher::Get(browser_context_));
  extension_registry_observation_.Observe(
      ExtensionRegistry::Get(browser_context_));

  StateStore* store = GetStateStore();
  if (store)
    store->RegisterKey(kBrowserActionStorageKey);
}

ExtensionActionStorageManager::~ExtensionActionStorageManager() = default;

void ExtensionActionStorageManager::OnExtensionLoaded(
    content::BrowserContext* browser_context,
    const Extension* extension) {
  ExtensionAction* action = ExtensionActionManager::Get(browser_context_)
                                ->GetExtensionAction(*extension);
  if (!action || action->action_type() != ActionInfo::Type::kBrowser) {
    return;
  }

  StateStore* store = GetStateStore();
  if (store) {
    store->GetExtensionValue(
        extension->id(), kBrowserActionStorageKey,
        base::BindOnce(&ExtensionActionStorageManager::ReadFromStorage,
                       weak_factory_.GetWeakPtr(), extension->id()));
  }
}

void ExtensionActionStorageManager::OnExtensionActionUpdated(
    ExtensionAction* extension_action,
    content::WebContents* web_contents,
    content::BrowserContext* browser_context) {
  // This is an update to the default settings of the action iff |web_contents|
  // is null. We only persist the default settings to disk, since per-tab
  // settings can't be persisted across browser sessions.
  bool for_default_tab = !web_contents;
  if (browser_context_ == browser_context &&
      extension_action->action_type() == ActionInfo::Type::kBrowser &&
      for_default_tab) {
    WriteToStorage(extension_action);
  }
}

void ExtensionActionStorageManager::OnShuttingDown() {
  extension_action_dispatcher_observation_.Reset();
}

void ExtensionActionStorageManager::WriteToStorage(
    ExtensionAction* extension_action) {
  StateStore* store = GetStateStore();
  if (store) {
    base::DictValue defaults = DefaultsToValue(extension_action);
    store->SetExtensionValue(extension_action->extension_id(),
                             kBrowserActionStorageKey,
                             base::Value(std::move(defaults)));
  }
}

void ExtensionActionStorageManager::ReadFromStorage(
    const ExtensionId& extension_id,
    std::optional<base::Value> value) {
  const Extension* extension = ExtensionRegistry::Get(browser_context_)->
      enabled_extensions().GetByID(extension_id);
  if (!extension)
    return;

  ExtensionAction* action = ExtensionActionManager::Get(browser_context_)
                                ->GetExtensionAction(*extension);
  if (!action || action->action_type() != ActionInfo::Type::kBrowser) {
    // This can happen if the extension is updated between startup and when the
    // storage read comes back, and the update removes the browser action.
    // http://crbug.com/40354327
    return;
  }

  if (!value || !value->is_dict())
    return;

  SetDefaultsFromValue(value->GetDict(), action);
}

StateStore* ExtensionActionStorageManager::GetStateStore() {
  return ExtensionSystem::Get(browser_context_)->state_store();
}

}  // namespace extensions
