// Copyright 2024 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_dispatcher.h"

#include "base/lazy_instance.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/sessions/core/session_id.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_action.h"
#include "extensions/browser/extension_action_manager.h"
#include "extensions/browser/extension_event_histogram_value.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/mojom/context_type.mojom.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

// Forwards events to the `context`'s event router.
void DispatchEventToExtension(
    content::BrowserContext* context,
    const ExtensionId& extension_id,
    events::HistogramValue histogram_value,
    const std::string& event_name,
    base::ListValue event_args,
    EventRouter::UserGestureState user_gesture_state) {
  auto* event_router = EventRouter::Get(context);
  if (!event_router) {
    return;
  }

  auto event = std::make_unique<Event>(histogram_value, event_name,
                                       std::move(event_args), context);
  event->user_gesture = user_gesture_state;
  event_router->DispatchEventToExtension(extension_id, std::move(event));
}

}  // namespace

static base::LazyInstance<
    BrowserContextKeyedAPIFactory<ExtensionActionDispatcher>>::DestructorAtExit
    g_extension_action_dispatcher_factory = LAZY_INSTANCE_INITIALIZER;

ExtensionActionDispatcher::ExtensionActionDispatcher(
    content::BrowserContext* context)
    : browser_context_(context) {}

ExtensionActionDispatcher::~ExtensionActionDispatcher() = default;

// static
BrowserContextKeyedAPIFactory<ExtensionActionDispatcher>*
ExtensionActionDispatcher::GetFactoryInstance() {
  return g_extension_action_dispatcher_factory.Pointer();
}

// static
ExtensionActionDispatcher* ExtensionActionDispatcher::Get(
    content::BrowserContext* context) {
  return BrowserContextKeyedAPIFactory<ExtensionActionDispatcher>::Get(context);
}

void ExtensionActionDispatcher::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void ExtensionActionDispatcher::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void ExtensionActionDispatcher::NotifyChange(ExtensionAction* extension_action,
                                             content::WebContents* web_contents,
                                             content::BrowserContext* context) {
  for (auto& observer : observers_) {
    observer.OnExtensionActionUpdated(extension_action, web_contents, context);
  }
}

void ExtensionActionDispatcher::DispatchExtensionActionClicked(
    const ExtensionAction& extension_action,
    content::WebContents* web_contents,
    const Extension* extension) {
  events::HistogramValue histogram_value = events::UNKNOWN;
  const char* event_name = nullptr;
  switch (extension_action.action_type()) {
    case ActionInfo::Type::kAction:
      histogram_value = events::ACTION_ON_CLICKED;
      event_name = "action.onClicked";
      break;
    case ActionInfo::Type::kBrowser:
      histogram_value = events::BROWSER_ACTION_ON_CLICKED;
      event_name = "browserAction.onClicked";
      break;
    case ActionInfo::Type::kPage:
      histogram_value = events::PAGE_ACTION_ON_CLICKED;
      event_name = "pageAction.onClicked";
      break;
  }

  if (event_name) {
    base::ListValue args;
    // The action APIs (browserAction, pageAction, action) are only available
    // to privileged extension contexts. As such, we deterministically know that
    // the right context type here is privileged.
    constexpr mojom::ContextType context_type =
        mojom::ContextType::kPrivilegedExtension;
    ExtensionTabUtil::ScrubTabBehavior scrub_tab_behavior =
        ExtensionTabUtil::GetScrubTabBehavior(extension, context_type,
                                              web_contents);
    args.Append(ExtensionTabUtil::CreateTabObject(web_contents,
                                                  scrub_tab_behavior, extension)
                    .ToValue());

    DispatchEventToExtension(web_contents->GetBrowserContext(),
                             extension_action.extension_id(), histogram_value,
                             event_name, std::move(args),
                             EventRouter::UserGestureState::kEnabled);
  }
}

void ExtensionActionDispatcher::ClearAllValuesForTab(
    content::WebContents* web_contents,
    bool web_contents_being_destroyed) {
  DCHECK(web_contents);
  const SessionID tab_id = sessions::SessionTabHelper::IdForTab(web_contents);
  content::BrowserContext* browser_context = web_contents->GetBrowserContext();
  const ExtensionSet& enabled_extensions =
      ExtensionRegistry::Get(browser_context_)->enabled_extensions();
  ExtensionActionManager* action_manager =
      ExtensionActionManager::Get(browser_context_);

  for (const auto& extension : enabled_extensions) {
    ExtensionAction* extension_action =
        action_manager->GetExtensionAction(*extension);
    if (extension_action) {
      extension_action->ClearAllValuesForTab(tab_id.id());
      // Declarative state is reverted by the ContentRulesRegistry on
      // navigations, but must be explicitly cleaned up on destruction.
      if (web_contents_being_destroyed) {
        extension_action->ClearDeclarativeValuesForTab(tab_id.id());
      }
      NotifyChange(extension_action, web_contents, browser_context);
    }
  }
}

ExtensionPrefs* ExtensionActionDispatcher::GetExtensionPrefs() {
  // This lazy initialization is more than just an optimization, because it
  // allows tests to associate a new ExtensionPrefs with the browser context
  // before we access it.
  if (!extension_prefs_) {
    extension_prefs_ = ExtensionPrefs::Get(browser_context_);
  }

  return extension_prefs_;
}


void ExtensionActionDispatcher::Shutdown() {
  for (auto& observer : observers_) {
    observer.OnShuttingDown();
  }
}

void ExtensionActionDispatcher::OnActionPinnedStateChanged(
    const ExtensionId& extension_id,
    bool is_pinned) {
  // TODO(crbug.com/360916928): Today, no action APIs are compiled.
  // Unfortunately, this means we miss out on the compiled types, which would be
  // rather helpful here.
  base::ListValue args;
  base::DictValue change;
  change.Set("isOnToolbar", is_pinned);
  args.Append(std::move(change));
  DispatchEventToExtension(browser_context_, extension_id,
                           events::ACTION_ON_USER_SETTINGS_CHANGED,
                           "action.onUserSettingsChanged", std::move(args),
                           EventRouter::UserGestureState::kNotEnabled);
}

}  // namespace extensions
