// Copyright 2022 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/api/side_panel/side_panel_service.h"

#include <cstddef>
#include <memory>
#include <optional>

#include "base/no_destructor.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/extensions/extension_side_panel_utils.h"
#include "chrome/common/extensions/api/side_panel.h"
#include "chrome/common/extensions/api/side_panel/side_panel_info.h"
#include "chrome/common/pref_names.h"
#include "components/sessions/core/session_id.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/pref_types.h"
#include "extensions/common/error_utils.h"

namespace extensions {

namespace {

// Key corresponding to whether the extension's side panel entry (if one exists)
// should be opened when its icon is clicked in the toolbar.
constexpr PrefMap kOpenSidePanelOnIconClickPref = {
    "open_side_panel_on_icon_click", PrefType::kBool,
    PrefScope::kExtensionSpecific};

api::side_panel::PanelOptions GetPanelOptionsFromManifest(
    const Extension& extension) {
  std::string path = SidePanelInfo::GetDefaultPath(&extension);
  api::side_panel::PanelOptions options;
  if (!path.empty()) {
    options.path = std::move(path);
    options.enabled = true;
  }
  return options;
}

}  // namespace

SidePanelService::~SidePanelService() = default;

SidePanelService::SidePanelService(content::BrowserContext* context)
    : browser_context_(context) {
  extensions::ExtensionRegistry* extension_registry =
      extensions::ExtensionRegistry::Get(context);
  extension_registry_observation_.Observe(extension_registry);
}

bool SidePanelService::HasSidePanelActionForTab(const Extension& extension,
                                                TabId tab_id) {
  if (!OpenSidePanelOnIconClick(extension.id())) {
    return false;
  }

  return HasSidePanelAvailableForTab(extension, tab_id);
}

bool SidePanelService::HasSidePanelContextMenuActionForTab(
    const Extension& extension,
    TabId tab_id) {
  return HasSidePanelAvailableForTab(extension, tab_id);
}

bool SidePanelService::HasSidePanelAvailableForTab(const Extension& extension,
                                                   TabId tab_id) {
  api::side_panel::PanelOptions options = GetOptions(extension, tab_id);
  return options.enabled.has_value() && *options.enabled &&
         options.path.has_value();
}

bool SidePanelService::HasContextualPanelAvailableForTab(
    const Extension& extension,
    TabId tab_id,
    bool verify_options) {
  auto panels_iter = panels_.find(extension.id());
  if (panels_iter != panels_.end()) {
    auto tab_panels_iter = panels_iter->second.find(tab_id);
    if (tab_panels_iter != panels_iter->second.end()) {
      const auto& options = tab_panels_iter->second;
      if (verify_options) {
        CHECK(options.path && options.enabled && *options.enabled);
      }
      return options.path.has_value() && options.enabled.has_value() &&
             options.enabled.value();
    }
  }
  return false;
}

api::side_panel::PanelOptions SidePanelService::GetOptions(
    const Extension& extension,
    std::optional<TabId> id) {
  auto extension_panel_options = panels_.find(extension.id());

  // Get default path from manifest if nothing was stored in this service for
  // the calling extension.
  if (extension_panel_options == panels_.end()) {
    return GetPanelOptionsFromManifest(extension);
  }

  TabId default_tab_id = SessionID::InvalidValue().id();
  TabId tab_id = id.has_value() ? id.value() : default_tab_id;
  TabPanelOptions& tab_panel_options = extension_panel_options->second;

  // The specific `tab_id` may have already been saved.
  if (tab_id != default_tab_id) {
    auto specific_tab_options = tab_panel_options.find(tab_id);
    if (specific_tab_options != tab_panel_options.end()) {
      return specific_tab_options->second.Clone();
    }
  }

  // Fall back to the default tab if no tab ID was specified or entries for the
  // specific tab weren't found.
  auto default_options = tab_panel_options.find(default_tab_id);
  if (default_options != tab_panel_options.end()) {
    return default_options->second.Clone();
  }

  // Fall back to the manifest-specified options as a last resort.
  return GetPanelOptionsFromManifest(extension);
}

api::side_panel::PanelOptions SidePanelService::GetSpecificOptionsForTab(
    const Extension& extension,
    TabId tab_id) {
  auto extension_panel_options = panels_.find(extension.id());
  if (extension_panel_options == panels_.end()) {
    return api::side_panel::PanelOptions();
  }

  TabPanelOptions& tab_panel_options = extension_panel_options->second;
  auto specific_tab_options = tab_panel_options.find(tab_id);
  return specific_tab_options == tab_panel_options.end()
             ? api::side_panel::PanelOptions()
             : specific_tab_options->second.Clone();
}

// Upsert to merge `panels_[extension_id][tab_id]` with `set_options`.
void SidePanelService::SetOptions(const Extension& extension,
                                  api::side_panel::PanelOptions options) {
  auto update_existing_options =
      [&options](api::side_panel::PanelOptions& existing_options) {
        if (options.path) {
          existing_options.path = std::move(options.path);
        }
        if (options.enabled) {
          existing_options.enabled = std::move(options.enabled);
        }
      };

  TabId tab_id = SessionID::InvalidValue().id();
  if (options.tab_id) {
    tab_id = *options.tab_id;
  }
  TabPanelOptions& extension_panel_options = panels_[extension.id()];
  auto it = extension_panel_options.find(tab_id);

  // Create the options if they don't exist, otherwise update them.
  if (it != extension_panel_options.end()) {
    update_existing_options(it->second);
  } else {
    // The default value for the optional enabled option is true. This default
    // is applied when the supplied option is being inserted for the first time.
    if (!options.enabled.has_value()) {
      options.enabled = true;
    }

    // If there is no entry for the default tab, merge `options` into the
    // manifest-specified options.
    if (tab_id == SessionID::InvalidValue().id()) {
      extension_panel_options[tab_id] = GetPanelOptionsFromManifest(extension);
      update_existing_options(extension_panel_options[tab_id]);
    } else {
      // Update an existing option.
      extension_panel_options[tab_id] = std::move(options);
    }
  }

  for (auto& observer : observers_) {
    observer.OnPanelOptionsChanged(extension.id(),
                                   extension_panel_options[tab_id]);
  }
}

bool SidePanelService::HasExtensionPanelOptionsForTest(const ExtensionId& id) {
  return panels_.count(id) != 0;
}

// static
BrowserContextKeyedAPIFactory<SidePanelService>*
SidePanelService::GetFactoryInstance() {
  static base::NoDestructor<BrowserContextKeyedAPIFactory<SidePanelService>>
      instance;
  return instance.get();
}

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

void SidePanelService::RemoveExtensionOptions(const ExtensionId& id) {
  panels_.erase(id);
}

bool SidePanelService::OpenSidePanelOnIconClick(
    const ExtensionId& extension_id) {
  bool open_side_panel_on_icon_click = false;
  // TODO(tjudkins): This should be taking in a browser context to read the pref
  // on, rather than using the one the service was created with.
  ExtensionPrefs::Get(browser_context_)
      ->ReadPrefAsBoolean(extension_id, kOpenSidePanelOnIconClickPref,
                          &open_side_panel_on_icon_click);
  return open_side_panel_on_icon_click;
}

void SidePanelService::SetOpenSidePanelOnIconClick(
    const ExtensionId& extension_id,
    bool open_side_panel_on_icon_click) {
  // TODO(tjudkins): This should be taking in a browser context to set the pref
  // on, rather than using the one the service was created with.
  ExtensionPrefs::Get(browser_context_)
      ->SetBooleanPref(extension_id, kOpenSidePanelOnIconClickPref,
                       open_side_panel_on_icon_click);
}

base::expected<bool, std::string> SidePanelService::OpenSidePanelForWindow(
    const Extension& extension,
    content::BrowserContext* context,
    int window_id,
    bool include_incognito_information) {
  std::string error;
  WindowController* window_controller =
      ExtensionTabUtil::GetControllerInProfileWithId(
          Profile::FromBrowserContext(context), window_id,
          include_incognito_information, &error);
  if (!window_controller) {
    return base::unexpected(error);
  }

  auto global_options = GetOptions(extension, std::nullopt);
  if (!global_options.path || !global_options.enabled.has_value() ||
      !(*global_options.enabled)) {
    return base::unexpected(
        base::StringPrintf("No active side panel for windowId: %d", window_id));
  }

  BrowserWindowInterface* browser_window =
      window_controller->GetBrowserWindowInterface();
  if (!browser_window) {
    return base::unexpected(
        base::StringPrintf("No browser window for windowId: %d", window_id));
  }

  side_panel_util::OpenGlobalExtensionSidePanel(
      *browser_window, /*web_contents=*/nullptr, extension.id());
  return true;
}

base::expected<bool, std::string> SidePanelService::OpenSidePanelForTab(
    const Extension& extension,
    content::BrowserContext* context,
    int tab_id,
    std::optional<int> window_id,
    bool include_incognito_information) {
  // First, find the corresponding tab.
  WindowController* window = nullptr;
  content::WebContents* web_contents = nullptr;
  if (!ExtensionTabUtil::GetTabById(tab_id, context,
                                    include_incognito_information, &window,
                                    &web_contents, nullptr) ||
      !window) {
    return base::unexpected(ErrorUtils::FormatErrorMessage(
        ExtensionTabUtil::kTabNotFoundError, base::ToString(tab_id)));
  }

  BrowserWindowInterface* browser_window = window->GetBrowserWindowInterface();
  if (!browser_window) {
    return base::unexpected(
        base::StringPrintf("No browser window for tabId: %d", tab_id));
  }

  // If both `tab_id` and `window_id` were provided, ensure the tab is in
  // the specified window. The window can also be null for prerender tabs
  // which can't have a side panel.
  if (window_id && window_id != window->GetWindowId()) {
    return base::unexpected(
        "The specified tab does not belong to the specified window.");
  }

  // Verify that an active side panel (contextual or global) exists for the tab.
  api::side_panel::PanelOptions panel_options = GetOptions(extension, tab_id);
  if (!panel_options.path || !panel_options.enabled.has_value() ||
      !(*panel_options.enabled)) {
    return base::unexpected(
        base::StringPrintf("No active side panel for tabId: %d", tab_id));
  }

  // Open the appropriate panel.
  if (HasContextualPanelAvailableForTab(extension, tab_id,
                                        /*verify_options=*/true)) {
    side_panel_util::OpenContextualExtensionSidePanel(
        *browser_window, *web_contents, extension.id());
  } else {
    side_panel_util::OpenGlobalExtensionSidePanel(*browser_window, web_contents,
                                                  extension.id());
  }

  return true;
}
void SidePanelService::DispatchOnClosedEvent(const ExtensionId& extension_id,
                                             int window_id,
                                             std::optional<int> tab_id,
                                             const std::string& path) {
  auto* router = EventRouter::Get(browser_context_);
  if (!router->ExtensionHasEventListener(
          extension_id, api::side_panel::OnClosed::kEventName)) {
    return;
  }

  base::ListValue args;
  api::side_panel::PanelClosedInfo info;
  info.window_id = window_id;
  info.tab_id = std::move(tab_id);
  info.path = path;
  args.Append(info.ToValue());

  auto event = std::make_unique<Event>(events::SIDE_PANEL_ON_CLOSED,
                                       api::side_panel::OnClosed::kEventName,
                                       std::move(args));

  router->DispatchEventToExtension(extension_id, std::move(event));
}

api::side_panel::PanelLayout SidePanelService::GetSidePanelLayout() {
  Profile* profile = Profile::FromBrowserContext(browser_context_);
  api::side_panel::PanelLayout layout;
  layout.side =
      profile->GetPrefs()->GetBoolean(prefs::kSidePanelHorizontalAlignment)
          ? api::side_panel::Side::kRight
          : api::side_panel::Side::kLeft;
  return layout;
}

base::expected<bool, std::string> SidePanelService::CloseSidePanelForTab(
    const Extension& extension,
    content::BrowserContext* context,
    int tab_id,
    std::optional<int> window_id,
    bool include_incognito_information) {
  WindowController* window = nullptr;
  content::WebContents* web_contents = nullptr;
  if (!ExtensionTabUtil::GetTabById(tab_id, context,
                                    include_incognito_information, &window,
                                    &web_contents, /*tab_index=*/nullptr) ||
      !web_contents) {
    return base::unexpected(ErrorUtils::FormatErrorMessage(
        ExtensionTabUtil::kTabNotFoundError, base::ToString(tab_id)));
  }

  // Retrieve the corresponding browser window, since the active side panel for
  // the tab might be a global one.
  BrowserWindowInterface* browser_window = window->GetBrowserWindowInterface();
  if (!browser_window) {
    return base::unexpected(
        base::StringPrintf("No browser window for tabId: %d", tab_id));
  }

  // Check that the given `tab_id` belongs to the given `window_id`.
  if (window_id.has_value() && *window_id != window->GetWindowId()) {
    return base::unexpected(
        "The specified tab does not belong to the specified window.");
  }

  // Verify that an active contextual side panel exists for the tab.
  if (!HasContextualPanelAvailableForTab(extension, tab_id,
                                         /*verify_options=*/false)) {
    return base::unexpected(base::StringPrintf(
        "No active tab-specific side panel for tabId: %d", tab_id));
  }

  side_panel_util::CloseContextualExtensionSidePanel(
      browser_window, web_contents, extension.id());
  return true;
}

base::expected<bool, std::string> SidePanelService::CloseSidePanelForWindow(
    const Extension& extension,
    content::BrowserContext* context,
    int window_id,
    bool include_incognito_information) {
  std::string error;
  WindowController* window_controller =
      ExtensionTabUtil::GetControllerInProfileWithId(
          Profile::FromBrowserContext(context), window_id,
          include_incognito_information, &error);
  if (!window_controller) {
    return base::unexpected(error);
  }

  // Verify that an active global side panel exists for the window.
  auto global_options = GetOptions(extension, /*tab_id=*/std::nullopt);
  if (!global_options.path || !global_options.enabled.value_or(false)) {
    return base::unexpected(
        base::StringPrintf("No active side panel for windowId: %d", window_id));
  }

  BrowserWindowInterface* browser_window =
      window_controller->GetBrowserWindowInterface();
  if (!browser_window) {
    return base::unexpected(
        base::StringPrintf("No browser window for windowId: %d", window_id));
  }

  side_panel_util::CloseGlobalExtensionSidePanel(browser_window,
                                                 extension.id());
  return true;
}

void SidePanelService::DispatchOnOpenedEvent(const ExtensionId& extension_id,
                                             int window_id,
                                             std::optional<int> tab_id,
                                             const std::string& path) {
  auto* router = EventRouter::Get(browser_context_);
  if (!router->ExtensionHasEventListener(
          extension_id, api::side_panel::OnOpened::kEventName)) {
    return;
  }

  api::side_panel::PanelOpenedInfo info;
  info.window_id = window_id;
  info.tab_id = std::move(tab_id);
  info.path = path;

  base::ListValue args;
  args.Append(info.ToValue());
  auto event = std::make_unique<Event>(events::SIDE_PANEL_ON_OPENED,
                                       api::side_panel::OnOpened::kEventName,
                                       std::move(args));
  router->DispatchEventToExtension(extension_id, std::move(event));
}

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

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

void SidePanelService::OnExtensionUnloaded(
    content::BrowserContext* browser_context,
    const Extension* extension,
    UnloadedExtensionReason reason) {
  RemoveExtensionOptions(extension->id());
}

void SidePanelService::OnExtensionUninstalled(
    content::BrowserContext* browser_context,
    const Extension* extension,
    UninstallReason reason) {
  RemoveExtensionOptions(extension->id());
}

void SidePanelService::Shutdown() {
  for (auto& observer : observers_) {
    observer.OnSidePanelServiceShutdown();
  }
}

}  // namespace extensions
