// Copyright 2020 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/tab_groups/tab_groups_api.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/strings/pattern.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/api/tabs/windows_util.h"
#include "chrome/browser/extensions/browser_extension_window_controller.h"
#include "chrome/browser/extensions/chrome_extension_function_details.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/common/extensions/api/tab_groups.h"
#include "chrome/common/extensions/api/tabs.h"
#include "chrome/common/extensions/api/windows.h"
#include "chrome/common/extensions/extension_constants.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "components/tabs/public/tab_group.h"
#include "components/tabs/public/tab_interface.h"
#include "extensions/buildflags/buildflags.h"
#include "ui/gfx/range/range.h"

#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "chrome/browser/ui/android/tab_model/tab_model_observer.h"
#endif  // BUILDFLAG(IS_ANDROID)

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

constexpr char kUnableToFindTabError[] = "Unable to find tab.";
constexpr char kCannotMoveGroupIntoMiddleOfOtherGroupError[] =
    "Cannot move the group to an index that is in the middle of another group.";
constexpr char kCannotMoveGroupIntoMiddleOfPinnedTabsError[] =
    "Cannot move the group to an index that is in the middle of pinned tabs.";
constexpr char kFailedToMoveGroupError[] = "Failed to move group.";

// Returns true if a group could be moved into the |target_index| of the given
// |tab_strip|. Sets the |error| string otherwise.
bool IndexSupportsGroupMove(TabListInterface* tab_list,
                            int target_index,
                            std::string* error) {
  // A group can always be moved to the end of the tabstrip.
  if (target_index >= tab_list->GetTabCount() || target_index < 0) {
    return true;
  }

  tabs::TabInterface* target_tab = tab_list->GetTab(target_index);
  if (!target_tab) {
    *error = kUnableToFindTabError;
    return false;
  }

  if (target_tab->IsPinned()) {
    *error = kCannotMoveGroupIntoMiddleOfPinnedTabsError;
    return false;
  }

  std::optional<tab_groups::TabGroupId> target_group = target_tab->GetGroup();

  // Get the group to the left of the target, if there is one.
  std::optional<tab_groups::TabGroupId> adjacent_group;
  if (target_index > 0) {
    tabs::TabInterface* adjacent_tab = tab_list->GetTab(target_index - 1);
    CHECK(adjacent_tab);
    adjacent_group = adjacent_tab->GetGroup();
  }
  if (target_group.has_value() && target_group == adjacent_group) {
    *error = kCannotMoveGroupIntoMiddleOfOtherGroupError;
    return false;
  }

  return true;
}

}  // namespace

ExtensionFunction::ResponseAction TabGroupsGetFunction::Run() {
  std::optional<api::tab_groups::Get::Params> params =
      api::tab_groups::Get::Params::Create(args());
  DCHECK(params.has_value());

  EXTENSION_FUNCTION_VALIDATE(params);
  int group_id = params->group_id;

  tab_groups::TabGroupId id = tab_groups::TabGroupId::CreateEmpty();
  tab_groups::TabGroupVisualData visual_data;
  std::string error;
  if (!ExtensionTabUtil::GetGroupById(group_id, browser_context(),
                                      include_incognito_information(), nullptr,
                                      &id, &visual_data, &error)) {
    return RespondNow(Error(std::move(error)));
  }

  DCHECK(!id.is_empty());

  return RespondNow(ArgumentList(api::tab_groups::Get::Results::Create(
      ExtensionTabUtil::CreateTabGroupObject(id, visual_data))));
}

ExtensionFunction::ResponseAction TabGroupsQueryFunction::Run() {
  std::optional<api::tab_groups::Query::Params> params =
      api::tab_groups::Query::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  base::ListValue result_list;
  Profile* profile = Profile::FromBrowserContext(browser_context());

  WindowController* window_controller =
      ChromeExtensionFunctionDetails(this).GetCurrentWindowController();
  if (!window_controller) {
    return RespondNow(Error(ExtensionTabUtil::kNoCurrentWindowError));
  }
  BrowserWindowInterface* current_browser =
      window_controller->GetBrowserWindowInterface();
  if (!current_browser) {
    return RespondNow(
        Error(ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError));
  }

  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&](BrowserWindowInterface* browser_window_interface) {
        if (!profile->IsSameOrParent(browser_window_interface->GetProfile())) {
          return true;
        }

        if (!include_incognito_information() &&
            profile != browser_window_interface->GetProfile()) {
          return true;
        }

        if (!BrowserExtensionWindowController::From(browser_window_interface)
                 ->IsVisibleToTabsAPIForExtension(
                     extension(), /*allow_dev_tools_windows=*/false)) {
          return true;
        }

        if (params->query_info.window_id) {
          const int window_id = *params->query_info.window_id;
          if (window_id >= 0 && window_id != ExtensionTabUtil::GetWindowId(
                                                 browser_window_interface)) {
            return true;
          }

          if (window_id == extension_misc::kCurrentWindowId &&
              browser_window_interface != current_browser) {
            return true;
          }
        }

        if (!ExtensionTabUtil::SupportsTabGroups(browser_window_interface)) {
          return true;
        }

        TabListInterface* tab_list =
            TabListInterface::From(browser_window_interface);
        if (!tab_list) {
          return true;
        }

        for (const tab_groups::TabGroupId& id : tab_list->ListTabGroups()) {
          std::optional<tab_groups::TabGroupVisualData> visual_data =
              tab_list->GetTabGroupVisualData(id);
          if (!visual_data) {
            continue;
          }

          if (params->query_info.collapsed &&
              *params->query_info.collapsed != visual_data->is_collapsed()) {
            continue;
          }

          if (params->query_info.title &&
              !base::MatchPattern(
                  visual_data->title(),
                  base::UTF8ToUTF16(*params->query_info.title))) {
            continue;
          }

          if (params->query_info.color != api::tab_groups::Color::kNone &&
              params->query_info.color !=
                  ExtensionTabUtil::ColorIdToColor(visual_data->color())) {
            continue;
          }

          if (params->query_info.shared.has_value() &&
              ExtensionTabUtil::GetSharedStateOfGroup(id) !=
                  params->query_info.shared.value()) {
            continue;
          }

          result_list.Append(
              ExtensionTabUtil::CreateTabGroupObject(id, *visual_data)
                  .ToValue());
        }
        return true;
      });

  return RespondNow(WithArguments(std::move(result_list)));
}

ExtensionFunction::ResponseAction TabGroupsUpdateFunction::Run() {
  std::optional<api::tab_groups::Update::Params> params =
      api::tab_groups::Update::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  int group_id = params->group_id;
  WindowController* window = nullptr;
  tab_groups::TabGroupId id = tab_groups::TabGroupId::CreateEmpty();
  tab_groups::TabGroupVisualData visual_data;
  std::string error;
  if (!ExtensionTabUtil::GetGroupById(group_id, browser_context(),
                                      include_incognito_information(), &window,
                                      &id, &visual_data, &error)) {
    return RespondNow(Error(std::move(error)));
  }

  // Since this is in a tab group, there should not be a prerender tab (with no
  // window).
  CHECK(window);

  DCHECK(!id.is_empty());

  bool collapsed = visual_data.is_collapsed();
  if (params->update_properties.collapsed) {
    collapsed = *params->update_properties.collapsed;
  }

  tab_groups::TabGroupColorId color = visual_data.color();
  if (params->update_properties.color != api::tab_groups::Color::kNone) {
    color = ExtensionTabUtil::ColorToColorId(params->update_properties.color);
  }

  std::u16string title = visual_data.title();
  if (params->update_properties.title) {
    title = base::UTF8ToUTF16(*params->update_properties.title);
  }

  if (!ExtensionTabUtil::IsTabStripEditable(*window->profile())) {
    return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError));
  }

  BrowserWindowInterface* browser = window->GetBrowserWindowInterface();
  if (!browser) {
    return RespondNow(
        Error(ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError));
  }

  if (!ExtensionTabUtil::SupportsTabGroups(browser)) {
    return RespondNow(
        Error(ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError));
  }

  // Update the visual data.
  auto* tab_list = TabListInterface::From(browser);
  if (!tab_list) {
    return RespondNow(
        Error(ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError));
  }
  tab_groups::TabGroupVisualData new_visual_data(title, color, collapsed);
  tab_list->SetTabGroupVisualData(id, new_visual_data);

  if (!has_callback()) {
    return RespondNow(NoArguments());
  }

  return RespondNow(ArgumentList(api::tab_groups::Get::Results::Create(
      ExtensionTabUtil::CreateTabGroupObject(id, new_visual_data))));
}

////////////////////////////////////////////////////////////////////////////////

#if BUILDFLAG(IS_ANDROID)
// Helper class to observe for tab group creation notifications. Used on Android
// because cross-window tab group moves are asynchronous.
class TabGroupsMoveFunction::ObserverHelper : public TabModelObserver {
 public:
  ObserverHelper(TabGroupsMoveFunction* owner, TabModel* tab_model)
      : owner_(owner), tab_model_(tab_model) {
    tab_model_->AddObserver(this);
  }

  ~ObserverHelper() override { tab_model_->RemoveObserver(this); }

  // TabModelObserver:
  void OnTabGroupCreated(tab_groups::TabGroupId group_id) override {
    owner_->OnTabGroupCreated(group_id);
  }

 private:
  raw_ptr<TabGroupsMoveFunction> owner_;
  raw_ptr<TabModel> tab_model_;
};
#endif  // BUILDFLAG(IS_ANDROID)

TabGroupsMoveFunction::TabGroupsMoveFunction() = default;

TabGroupsMoveFunction::~TabGroupsMoveFunction() = default;

ExtensionFunction::ResponseAction TabGroupsMoveFunction::Run() {
  std::optional<api::tab_groups::Move::Params> params =
      api::tab_groups::Move::Params::Create(args());
  EXTENSION_FUNCTION_VALIDATE(params);

  int group_id = params->group_id;
  int new_index = params->move_properties.index;
  const auto& window_id = params->move_properties.window_id;

  tab_groups::TabGroupId group = tab_groups::TabGroupId::CreateEmpty();
  bool cross_window = false;
  std::string error;
  const bool group_moved =
      MoveGroup(group_id, new_index, window_id, &group, &cross_window, &error);
  if (!group_moved) {
    return RespondNow(Error(std::move(error)));
  }

#if BUILDFLAG(IS_ANDROID)
  if (cross_window) {
    // Cross window group moves are asynchronous on Android. OnTabGroupCreated()
    // will be called later when the group is created in the new window.
    AddRef();  // Balanced in OnTabGroupCreated().
    return RespondLater();
  }
#endif  // BUILDFLAG(IS_ANDROID)

  if (!has_callback()) {
    return RespondNow(NoArguments());
  }

  auto group_object = ExtensionTabUtil::CreateTabGroupObject(group);
  CHECK(group_object);
  return RespondNow(
      ArgumentList(api::tab_groups::Get::Results::Create(*group_object)));
}

bool TabGroupsMoveFunction::MoveGroup(int group_id,
                                      int new_index,
                                      const std::optional<int>& window_id,
                                      tab_groups::TabGroupId* group,
                                      bool* cross_window,
                                      std::string* error) {
  WindowController* source_window = nullptr;
  tab_groups::TabGroupVisualData visual_data;
  if (!ExtensionTabUtil::GetGroupById(
          group_id, browser_context(), include_incognito_information(),
          &source_window, group, &visual_data, error)) {
    return false;
  }

  if (!ExtensionTabUtil::IsTabStripEditable(*source_window->profile())) {
    *error = ExtensionTabUtil::kTabStripNotEditableError;
    return false;
  }

  auto* source_browser = source_window->GetBrowserWindowInterface();
  if (!source_browser) {
    *error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
    return false;
  }

  if (!ExtensionTabUtil::SupportsTabGroups(source_browser)) {
    *error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
    return false;
  }

  TabListInterface* source_tab_list = TabListInterface::From(source_browser);
  if (!source_tab_list) {
    *error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
    return false;
  }

  gfx::Range tabs = source_tab_list->GetTabGroupTabIndices(*group);
  if (tabs.length() == 0) {
    return false;
  }

  if (window_id) {
    WindowController* target_window = nullptr;
    if (!windows_util::GetControllerFromWindowID(
            this, *window_id, WindowController::GetAllWindowFilter(),
            &target_window, error)) {
      return false;
    }
    BrowserWindowInterface* target_browser =
        target_window->GetBrowserWindowInterface();
    if (!target_browser) {
      *error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
      return false;
    }

    // TODO(crbug.com/40638654): Rather than calling GetType() ==
    // BrowserWindowInterface::Type::TYPE_NORMAL, should this call
    // SupportsWindowFeature(Browser::kFeatureTabstrip)?
    if (target_browser->GetType() != BrowserWindowInterface::TYPE_NORMAL) {
      *error = ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError;
      return false;
    }

    if (target_window->profile() != source_window->profile()) {
      *error = ExtensionTabUtil::kCanOnlyMoveTabsWithinSameProfileError;
      return false;
    }

    // If windowId is different from the current window, move between windows.
    if (target_browser != source_browser) {
      *cross_window = true;
#if BUILDFLAG(IS_ANDROID)
      // Observe for OnTabGroupCreated() notifications, because cross-window
      // moves are asynchronous on Android.
      TabModel* target_tab_model =
          TabModelList::FindTabModelWithWindowSessionId(
              target_browser->GetSessionID());
      if (!target_tab_model) {
        *error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
        return false;
      }
      observer_helper_ =
          std::make_unique<ObserverHelper>(this, target_tab_model);
#endif  // BUILDFLAG(IS_ANDROID)
      return MoveTabGroupBetweenBrowsers(source_browser, target_browser, *group,
                                         visual_data, tabs, new_index, error);
    }
  }

  // Perform a move within the same window.

  // When moving to the right, adjust the target index for the size of the
  // group, since the group itself may occupy several indices to the right.
  const int start_index = tabs.start();
  const int new_index_before_group_is_removed =
      new_index > start_index ? new_index + tabs.length() : new_index;

  if (!IndexSupportsGroupMove(source_tab_list,
                              new_index_before_group_is_removed, error)) {
    return false;
  }

  // Unlike when moving between windows, the index should be clamped to
  // count() - (#num of tabs in group being moved). Since the current tab(s)
  // being moved are within the same tabstrip, they can't be added beyond the
  // end of the occupied indices, but rather just shifted among them.
  const int size_after_group_removed =
      source_tab_list->GetTabCount() - tabs.length();
  if (new_index >= size_after_group_removed || new_index < 0) {
    new_index = size_after_group_removed;
  }

  if (new_index == start_index) {
    return true;
  }

  source_tab_list->MoveGroupTo(*group, new_index);

  return true;
}

bool TabGroupsMoveFunction::MoveTabGroupBetweenBrowsers(
    BrowserWindowInterface* source_browser,
    BrowserWindowInterface* target_browser,
    const tab_groups::TabGroupId& group,
    const tab_groups::TabGroupVisualData& visual_data,
    const gfx::Range& tabs,
    int new_index,
    std::string* error) {
  TabListInterface* target_tab_list =
      ExtensionTabUtil::GetEditableTabList(*target_browser);
  if (!target_tab_list) {
    *error = ExtensionTabUtil::kTabStripNotEditableError;
    return false;
  }

  if (!ExtensionTabUtil::SupportsTabGroups(target_browser)) {
    *error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
    return false;
  }

  if (new_index > target_tab_list->GetTabCount() || new_index < 0) {
    new_index = target_tab_list->GetTabCount();
  }

  if (!IndexSupportsGroupMove(target_tab_list, new_index, error)) {
    return false;
  }

  TabListInterface* source_tab_list = TabListInterface::From(source_browser);
  if (!source_tab_list) {
    *error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError;
    return false;
  }

  // Pausing Saved Tab Groups is handled in TabListBridge on Win/Mac/Linux and
  // in MultiInstanceManagerApi31 on Android.
  if (!source_tab_list->MoveTabGroupToWindow(
          group, target_browser->GetSessionID(), new_index)) {
    *error = kFailedToMoveGroupError;
    return false;
  }

  return true;
}

#if BUILDFLAG(IS_ANDROID)
void TabGroupsMoveFunction::OnTabGroupCreated(tab_groups::TabGroupId group_id) {
  observer_helper_.reset();

  auto group_object = ExtensionTabUtil::CreateTabGroupObject(group_id);
  CHECK(group_object);
  Respond(ArgumentList(api::tab_groups::Get::Results::Create(*group_object)));

  Release();  // Balanced in MoveGroup().
}
#endif  // BUILDFLAG(IS_ANDROID)

}  // namespace extensions
