// 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.

#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"

#import "base/apple/foundation_util.h"
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"  // IDC_BOOKMARK_MENU
#import "chrome/browser/app_controller_mac.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_merged_surface_service_observer.h"
#include "chrome/browser/bookmarks/bookmark_parent_folder.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/bookmarks/bookmark_stats.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils_desktop.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/create_browser_window.h"
#include "chrome/browser/ui/browser_window/public/profile_browser_collection.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
#import "chrome/browser/ui/cocoa/l10n_util.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/profile_metrics/browser_profile_type.h"
#import "ui/base/cocoa/cocoa_base_utils.h"
#include "ui/base/window_open_disposition.h"
#import "ui/menus/cocoa/menu_controller.h"

using base::UserMetricsAction;
using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
using content::OpenURLParams;
using content::Referrer;

namespace {

// Returns the NSMenuItem in |submenu|'s supermenu that holds |submenu|.
NSMenuItem* GetItemWithSubmenu(NSMenu* submenu) {
  NSArray* parent_items = [[submenu supermenu] itemArray];
  for (NSMenuItem* item in parent_items) {
    if ([item submenu] == submenu) {
      return item;
    }
  }
  return nil;
}

const BookmarkNode* GetNodeByUuid(const BookmarkModel* model,
                                  const base::Uuid& guid) {
  CHECK(model);
  const BookmarkNode* node = model->GetNodeByUuid(
      guid, BookmarkModel::NodeTypeForUuidLookup::kAccountNodes);
  if (!node) {
    node = model->GetNodeByUuid(
        guid, BookmarkModel::NodeTypeForUuidLookup::kLocalOrSyncableNodes);
  }
  return node;
}

void DoOpenBookmark(Profile* profile,
                    WindowOpenDisposition disposition,
                    const BookmarkNode* node) {
  DCHECK(profile);
  BrowserWindowInterface* browser_window_interface =
      ProfileBrowserCollection::GetForProfile(profile)->FindTabbedBrowser(
          /*match_original_profiles=*/true);
  Browser* browser =
      browser_window_interface
          ? browser_window_interface->GetBrowserForMigrationOnly()
          : nullptr;
  if (!browser) {
    browser = CreateBrowserWindow(BrowserWindowCreateParams(profile, true))
                  ->GetBrowserForMigrationOnly();
  }
  OpenURLParams params(node->url(), Referrer(), disposition,
                       ui::PAGE_TRANSITION_AUTO_BOOKMARK, false);
  browser->OpenURL(params, /*navigation_handle_callback=*/{});
  RecordBookmarkLaunch(BookmarkLaunchLocation::kTopMenu,
                       profile_metrics::GetBrowserProfileType(profile));
}

// Waits for the BookmarkMergedSurfaceServiceLoaded(), then calls
// DoOpenBookmark() on it.
//
// Owned by itself. Allocate with `new`.
class BookmarkRestorer : public BookmarkMergedSurfaceServiceObserver {
 public:
  BookmarkRestorer(Profile* profile,
                   WindowOpenDisposition disposition,
                   base::Uuid guid);
  ~BookmarkRestorer() override = default;

  // BookmarkMergedSurfaceServiceObserver:
  void BookmarkMergedSurfaceServiceLoaded() override;
  void BookmarkMergedSurfaceServiceBeingDeleted() override;
  void BookmarkNodeAdded(const BookmarkParentFolder& parent,
                         size_t index) override {}
  void BookmarkNodesRemoved(
      const BookmarkParentFolder& parent,
      const base::flat_set<const bookmarks::BookmarkNode*>& nodes) override {}
  void BookmarkNodeMoved(const BookmarkParentFolder& old_parent,
                         size_t old_index,
                         const BookmarkParentFolder& new_parent,
                         size_t new_index) override {}
  void BookmarkNodeChanged(const bookmarks::BookmarkNode* node) override {}
  void BookmarkNodeFaviconChanged(
      const bookmarks::BookmarkNode* node) override {}
  void BookmarkParentFolderChildrenReordered(
      const BookmarkParentFolder& folder) override {}
  void BookmarkAllUserNodesRemoved() override {}

 private:
  const raw_ptr<Profile> profile_;
  const WindowOpenDisposition disposition_;
  const base::Uuid guid_;
  raw_ptr<BookmarkMergedSurfaceService> bookmark_service_;
  base::ScopedObservation<BookmarkMergedSurfaceService,
                          BookmarkMergedSurfaceServiceObserver>
      observation_{this};
};

BookmarkRestorer::BookmarkRestorer(Profile* profile,
                                   WindowOpenDisposition disposition,
                                   base::Uuid guid)
    : profile_(profile), disposition_(disposition), guid_(guid) {
  bookmark_service_ =
      BookmarkMergedSurfaceServiceFactory::GetForProfile(profile);
  CHECK(bookmark_service_);
  observation_.Observe(bookmark_service_);
}

void BookmarkRestorer::BookmarkMergedSurfaceServiceLoaded() {
  const BookmarkModel* model = bookmark_service_->bookmark_model();
  if (const BookmarkNode* node = GetNodeByUuid(model, guid_)) {
    DoOpenBookmark(profile_, disposition_, node);
  }
  delete this;
}

void BookmarkRestorer::BookmarkMergedSurfaceServiceBeingDeleted() {
  delete this;
}

// Open the URL of the given BookmarkNode in the current tab. Waits for
// BookmarkModelLoaded() if needed (e.g. for a freshly-loaded profile).
void OpenBookmarkByGUID(WindowOpenDisposition disposition,
                        base::Uuid guid,
                        Profile* profile) {
  if (!profile) {
    // Failed to load profile, ignore.
    return;
  }

  BookmarkMergedSurfaceService* bookmark_service =
      BookmarkMergedSurfaceServiceFactory::GetForProfile(profile);
  CHECK(bookmark_service);

  if (!bookmark_service->loaded()) {
    // Bookmark service hasn't loaded yet. Wait for
    // BookmarkMergedSurfaceServiceLoaded(), and *then* open it.
    std::ignore = new BookmarkRestorer(profile, disposition, std::move(guid));
    return;
  }

  const BookmarkNode* node =
      GetNodeByUuid(bookmark_service->bookmark_model(), guid);
  if (!node) {
    // Bookmark not known, ignore.
    return;
  }

  // BookmarkModel already loaded and the bookmark is known. Open it
  // immediately.
  DoOpenBookmark(profile, disposition, node);
}

}  // namespace

@implementation BookmarkMenuCocoaController {
  raw_ptr<BookmarkMenuBridge, AcrossTasksDanglingUntriaged>
      _bridge;  // Weak. Owns |self|.
}

+ (NSString*)tooltipForNode:(const BookmarkNode*)node {
  NSString* title = base::SysUTF16ToNSString(node->GetTitle());
  if (node->is_folder()) {
    return title;
  }
  std::string urlString = node->url().possibly_invalid_spec();
  NSString* url = base::SysUTF8ToNSString(urlString);
  return cocoa_l10n_util::TooltipForURLAndTitle(url, title);
}

- (instancetype)initWithBridge:(BookmarkMenuBridge*)bridge {
  if ((self = [super init])) {
    _bridge = bridge;
    DCHECK(_bridge);
  }
  return self;
}

- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
  return ![AppController.sharedController keyWindowIsModal];
}

// NSMenu delegate method: called just before menu is displayed.
- (void)menuNeedsUpdate:(NSMenu*)menu {
  Profile* profile = _bridge->GetProfile();
  if (!profile) {
    // Unfortunately, we can't update a menu with a dead profile.
    return;
  }

  // The root menu does not have a corresponding bookmark node.
  if (_bridge->IsMenuRoot(menu)) {
    _bridge->UpdateRootMenuIfInvalid();
    return;
  }

  // Find the bookmark node corresponding to this menu.
  const BookmarkModel* model =
      BookmarkMergedSurfaceServiceFactory::GetForProfile(profile)
          ->bookmark_model();
  NSMenuItem* item = GetItemWithSubmenu(menu);
  base::Uuid guid = _bridge->TagToGUID([item tag]);
  const BookmarkNode* node = GetNodeByUuid(model, guid);

  if (!(node && node->is_folder())) {
    // TODO(crbug.com/417269167): every non-root menu must correspond to a
    // bookmark folder by construction.
    SCOPED_CRASH_KEY_NUMBER("BookmarkMenuCocoaController", "NSMenuItem",
                            [item tag]);
    SCOPED_CRASH_KEY_STRING32("BookmarkMenuCocoaController", "NSMenuItem",
                              base::SysNSStringToUTF8([item title]));
    base::debug::DumpWithoutCrashing();
    return;
  }

  _bridge->UpdateNonRootMenu(menu, BookmarkParentFolder::FromFolderNode(node));
}

- (BOOL)menuHasKeyEquivalent:(NSMenu*)menu
                    forEvent:(NSEvent*)event
                      target:(id*)target
                      action:(SEL*)action {
  // Note it is OK to return NO if there's already an item in |menu| that
  // handles |event|.
  return NO;
}

// Open the URL of the given BookmarkNode in the current tab. If the Profile
// is not loaded in memory, load it first.
- (void)openURLForGUID:(base::Uuid)guid {
  WindowOpenDisposition disposition =
      ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
  if (Profile* profile = _bridge->GetProfile()) {
    OpenBookmarkByGUID(disposition, std::move(guid), profile);
  } else {
    // Both BookmarkMenuBridge and BookmarkMenuCocoaController could get
    // destroyed before RunInSafeProfileHelper finishes. The callback needs to
    // be self-contained.
    app_controller_mac::RunInProfileSafely(
        _bridge->GetProfileDir(),
        base::BindOnce(&OpenBookmarkByGUID, disposition, std::move(guid)),
        app_controller_mac::kIgnoreOnFailure);
  }
}

- (IBAction)openBookmarkMenuItem:(id)sender {
  NSInteger tag = [sender tag];
  base::Uuid guid = _bridge->TagToGUID(tag);
  [self openURLForGUID:std::move(guid)];
}

+ (void)openBookmarkByGUID:(base::Uuid)guid
                 inProfile:(Profile*)profile
           withDisposition:(WindowOpenDisposition)disposition {
  OpenBookmarkByGUID(disposition, guid, profile);
}

@end  // BookmarkMenuCocoaController
