// 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 "chrome/browser/ui/bookmarks/bookmark_utils.h"

#include <stddef.h>

#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/bookmarks/bookmark_merged_surface_service.h"
#include "chrome/browser/bookmarks/bookmark_merged_surface_service_factory.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node_data.h"
#include "components/bookmarks/common/bookmark_bar_visibility_state.h"
#include "components/bookmarks/common/bookmark_pref_names.h"
#include "components/bookmarks/managed/managed_bookmark_service.h"
#include "components/dom_distiller/core/url_constants.h"
#include "components/dom_distiller/core/url_utils.h"
#include "components/prefs/pref_service.h"
#include "components/saved_tab_groups/public/features.h"
#include "components/search/ntp_features.h"
#include "components/search/search.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/url_formatter.h"
#include "components/user_prefs/user_prefs.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/drop_target_event.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/pointer/touch_ui_controller.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_variant.h"

#if defined(TOOLKIT_VIEWS)
#include "chrome/grit/theme_resources.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/themed_vector_icon.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.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/paint_vector_icon.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/resources/grit/ui_resources.h"
#endif

namespace chrome {
namespace {

using ::bookmarks::BookmarkModel;
using ::bookmarks::BookmarkNode;
using ::ui::mojom::DragOperation;
using PermanentFolderType = BookmarkParentFolder::PermanentFolderType;

#if defined(TOOLKIT_VIEWS)
// Image source that flips the supplied source image in RTL.
class RTLFlipSource : public gfx::ImageSkiaSource {
 public:
  explicit RTLFlipSource(gfx::ImageSkia source) : source_(std::move(source)) {}
  ~RTLFlipSource() override = default;

  // gfx::ImageSkiaSource:
  gfx::ImageSkiaRep GetImageForScale(float scale) override {
    gfx::Canvas canvas(source_.size(), scale, false);
    gfx::ScopedCanvas scoped_canvas(&canvas);
    scoped_canvas.FlipIfRTL(source_.width());
    canvas.DrawImageInt(source_, 0, 0);
    return gfx::ImageSkiaRep(canvas.GetBitmap(), scale);
  }

 private:
  const gfx::ImageSkia source_;
};
#endif  // defined(TOOLKIT_VIEWS)

// Returns true if the `dragged_node` can be dropped on `drop_parent` at
// `index`. A drop is not allowed on a managed node. A drop from a separate
// profile or a URL is allowed, where as a drop from the same profile is only
// allowed if:
// - The `dragged_node` is not an ancestor of `drop_parent`
// - The `dragged _node` isn't already a child of `drop_parent` at `index`.
bool IsValidBookmarkDropLocation(
    const BookmarkMergedSurfaceService* bookmark_merged_service,
    const bookmarks::BookmarkNode* dragged_node,
    bool dragged_from_same_profile,
    const BookmarkParentFolder& drop_parent,
    size_t index) {
  if (bookmark_merged_service->IsParentFolderManaged(drop_parent)) {
    // Drop on a managed node is not allowed.
    return false;
  }

  if (!dragged_from_same_profile) {
    // If the drop is not on a managed node, always accept if node from another
    // profile or the user is dragging a URL.
    // Note: It is expected if the user is dragging a URL
    // `dragged_from_same_profile` is false as a URL is not associated with a
    // profile.
    return true;
  }

  // In general, `dragged_node` is null if the node is from another profile or
  // the user is dragging a url. In both cases, `dragged_from_same_profile` is
  // expected to be false. For dragging a node within the same profile, the
  // `dragged_node` must be not null.
  // However, there’s an edge case. `dragged_node` might get deleted and become
  // null during the drag operation, and it only happens when using the
  // `chrome.bookmarks` extension API. In this case, return false to cancel the
  // current drag operation.
  // See https://crbug.com/472376579
  if (!dragged_node) {
    return false;
  }

  CHECK(!dragged_node->is_root());
  CHECK(!dragged_node->is_permanent_node());
  // Don't allow the drop if the user is attempting to drop on the node being
  // dragged.
  if (drop_parent.HasDirectChildNode(dragged_node)) {
    // Reordering.
    size_t node_index = bookmark_merged_service->GetIndexOf(dragged_node);
    if (index == node_index || index == node_index + 1) {
      return false;
    }
  }

  // drop_parent can't accept a child that is an ancestor.
  if (drop_parent.HoldsNonPermanentFolder() &&
      drop_parent.as_non_permanent_folder()->HasAncestor(dragged_node)) {
    return false;
  }

  return true;
}

}  // namespace

GURL GetURLToBookmark(content::WebContents* web_contents) {
  DCHECK(web_contents);
  if (search::IsInstantNTP(web_contents)) {
    return GURL(kChromeUINewTabURL);
  }
  // Users cannot bookmark Reader Mode pages directly, so the bookmark
  // interaction is as if it were with the original page.
  if (dom_distiller::url_utils::IsDistilledPage(
          web_contents->GetVisibleURL())) {
    return dom_distiller::url_utils::GetOriginalUrlFromDistillerUrl(
        web_contents->GetVisibleURL());
  }
  return web_contents->GetVisibleURL();
}

bool GetURLAndTitleToBookmark(content::WebContents* web_contents,
                              GURL* url,
                              std::u16string* title) {
  GURL u = GetURLToBookmark(web_contents);
  if (!u.is_valid()) {
    return false;
  }
  *url = u;
  if (dom_distiller::url_utils::IsDistilledPage(
          web_contents->GetVisibleURL())) {
    // Users cannot bookmark Reader Mode pages directly. Instead, a bookmark
    // is added for the original page and original title.
    *title =
        base::UTF8ToUTF16(dom_distiller::url_utils::GetTitleFromDistillerUrl(
            web_contents->GetVisibleURL()));
  } else {
    *title = web_contents->GetTitle();
  }

  // Use "New tab" as title if the current page is NTP even in incognito mode.
  if (u == chrome::ChromeUINewTabURLAsGURL()) {
    *title = l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE);
  }

  return true;
}

void ToggleBookmarkBarWhenVisible(content::BrowserContext* browser_context) {
  PrefService* prefs = user_prefs::UserPrefs::Get(browser_context);
  // The user changed when the bookmark bar is shown, update the preferences.
  if (base::FeatureList::IsEnabled(
          ntp_features::kNtpSimplificationBookmarkBar)) {
    auto current_state = static_cast<bookmarks::BookmarkBarVisibilityState>(
        prefs->GetInteger(bookmarks::prefs::kBookmarkBarVisibilityState));
    if (current_state == bookmarks::BookmarkBarVisibilityState::kAlwaysShow) {
      prefs->SetInteger(
          bookmarks::prefs::kBookmarkBarVisibilityState,
          static_cast<int>(bookmarks::BookmarkBarVisibilityState::kAlwaysHide));
    } else {
      prefs->SetInteger(
          bookmarks::prefs::kBookmarkBarVisibilityState,
          static_cast<int>(bookmarks::BookmarkBarVisibilityState::kAlwaysShow));
    }
  } else {
    const bool always_show =
        !prefs->GetBoolean(bookmarks::prefs::kShowBookmarkBar);
    prefs->SetBoolean(bookmarks::prefs::kShowBookmarkBar, always_show);
  }
}

// Called upon direct user interaction with the Bookmarks Bar UI (e.g. clicking
// a bookmark/folder, interacting with saved tab groups, or modifying items via
// context menu/drag-and-drop).
//
// For users in the NTP Simplification transition period (`IsDefaultValue()` is
// true), this explicitly sets `kBookmarkBarVisibilityState` to
// `kOnlyShowOnNtp`. This establishes the user store as the controlling
// preference store, transitioning the user out of the experiment's default
// state and preventing future auto-hiding.
//
// Note: Users who had `kShowBookmarkBar` set to true prior to the experiment
// are upgraded to `kAlwaysShow` at startup in `BookmarkBarController`, making
// `IsDefaultValue()` false. Therefore, `IsDefaultValue()` is only true here for
// users in the "off by default" / "only show on NTP" transition group.
void UpdateBookmarkBarVisibilityPrefOnUserAction(Profile* profile) {
  if (!profile || !profile->GetPrefs()) {
    return;
  }

  if (!base::FeatureList::IsEnabled(
          ntp_features::kNtpSimplificationBookmarkBar)) {
    return;
  }

  PrefService* prefs = profile->GetPrefs();
  const PrefService::Preference* state_pref =
      prefs->FindPreference(bookmarks::prefs::kBookmarkBarVisibilityState);

  if (state_pref && state_pref->IsDefaultValue()) {
    prefs->SetInteger(
        bookmarks::prefs::kBookmarkBarVisibilityState,
        static_cast<int>(
            bookmarks::BookmarkBarVisibilityState::kOnlyShowOnNtp));
  }
}

std::u16string FormatBookmarkURLForDisplay(const GURL& url) {
  // Because this gets re-parsed by FixupURL(), it's safe to omit the scheme
  // and trailing slash, and unescape most characters. However, it's
  // important not to drop any username/password, or unescape anything that
  // changes the URL's meaning.
  url_formatter::FormatUrlTypes format_types =
      url_formatter::kFormatUrlOmitDefaults &
      ~url_formatter::kFormatUrlOmitUsernamePassword;

  // If username is present, we must not omit the scheme because FixupURL() will
  // subsequently interpret the username as a scheme. crbug.com/40085150
  if (url.has_username()) {
    format_types &= ~url_formatter::kFormatUrlOmitHTTP;
  }

  return url_formatter::FormatUrl(url, format_types, base::UnescapeRule::SPACES,
                                  nullptr, nullptr, nullptr);
}

bool IsAppsShortcutEnabled(Profile* profile) {
#if BUILDFLAG(IS_CHROMEOS)
  // Chrome OS uses the app list / app launcher.
  return false;
#else
  return search::IsInstantExtendedAPIEnabled() && !profile->IsOffTheRecord();
#endif
}

bool ShouldShowAppsShortcutInBookmarkBar(Profile* profile) {
  return IsAppsShortcutEnabled(profile) &&
         profile->GetPrefs()->GetBoolean(
             bookmarks::prefs::kShowAppsShortcutInBookmarkBar);
}

bool ShouldShowTabGroupsInBookmarkBar(Profile* profile) {
  return profile->GetPrefs()->GetBoolean(
      bookmarks::prefs::kShowTabGroupsInBookmarkBar);
}

int GetBookmarkDragOperation(content::BrowserContext* browser_context,
                             const BookmarkNode* node) {
  PrefService* prefs = user_prefs::UserPrefs::Get(browser_context);
  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContext(browser_context);

  int move = ui::DragDropTypes::DRAG_MOVE;
  if (!prefs->GetBoolean(bookmarks::prefs::kEditBookmarksEnabled) ||
      model->client()->IsNodeManaged(node)) {
    move = ui::DragDropTypes::DRAG_NONE;
  }
  if (node->is_url()) {
    return ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_LINK | move;
  }
  return ui::DragDropTypes::DRAG_COPY | move;
}

DragOperation GetPreferredBookmarkDropOperation(int source_operations,
                                                int operations) {
  int common_ops = (source_operations & operations);
  if (!common_ops) {
    return DragOperation::kNone;
  }
  if (ui::DragDropTypes::DRAG_COPY & common_ops) {
    return DragOperation::kCopy;
  }
  if (ui::DragDropTypes::DRAG_LINK & common_ops) {
    return DragOperation::kLink;
  }
  if (ui::DragDropTypes::DRAG_MOVE & common_ops) {
    return DragOperation::kMove;
  }
  return DragOperation::kNone;
}

DragOperation GetBookmarkDropOperation(Profile* profile,
                                       const ui::DropTargetEvent& event,
                                       const bookmarks::BookmarkNodeData& data,
                                       const BookmarkParentFolder& parent,
                                       size_t index) {
  if (data.size() != 1) {
    // Currently only accept one dragged element (bookmark node/url) at a time.
    return DragOperation::kNone;
  }

  BookmarkMergedSurfaceService* const bookmark_merged_service =
      BookmarkMergedSurfaceServiceFactory::GetForProfile(profile);
  const BookmarkNode* const dragged_node = data.GetFirstNode(
      bookmark_merged_service->bookmark_model(), profile->GetPath());
  if (!IsValidBookmarkDropLocation(bookmark_merged_service, dragged_node,
                                   data.IsFromProfilePath(profile->GetPath()),
                                   parent, index)) {
    return DragOperation::kNone;
  }

  if (dragged_node) {
    // User is dragging from this profile.
    if (bookmark_merged_service->IsNodeManaged(dragged_node)) {
      // Do a copy instead of a move when dragging bookmarks that the user can't
      // modify.
      return DragOperation::kCopy;
    }
    return DragOperation::kMove;
  }

  // User is dragging from another app, copy.
  return GetPreferredBookmarkDropOperation(
      event.source_operations(),
      ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_LINK);
}

bool CanAllBeEditedByUser(
    bookmarks::ManagedBookmarkService* managed_bookmark_service,
    const std::vector<
        raw_ptr<const bookmarks::BookmarkNode, VectorExperimental>>& nodes) {
  if (!managed_bookmark_service) {
    return true;
  }

  for (const bookmarks::BookmarkNode* node : nodes) {
    if (managed_bookmark_service->IsNodeManaged(node)) {
      return false;
    }
  }
  return true;
}

#if defined(TOOLKIT_VIEWS)

ui::ImageModel GetBookmarkFolderIcon(BookmarkFolderIconType icon_type,
                                     ui::ColorVariant color) {
  const gfx::VectorIcon* icon_id;
  if (icon_type == BookmarkFolderIconType::kNormal) {
    icon_id = &(features::IsRoundedIconsEnabled()
                    ? vector_icons::kFolderFlippableIcon
                    : vector_icons::kFolderChromeRefreshOldIcon);
  } else {
    icon_id = &(features::IsRoundedIconsEnabled()
                    ? vector_icons::kFolderManagedFlippableIcon
                    : vector_icons::kFolderManagedRefreshOldIcon);
  }
  return ui::ImageModel::FromVectorIcon(*icon_id, color);
}
#endif

}  // namespace chrome
