// 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/cocoa/bookmarks/bookmark_menu_bridge.h"

#include "base/strings/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#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_parent_folder.h"
#include "chrome/browser/bookmarks/bookmark_parent_folder_children.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/prefs/incognito_mode_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils_desktop.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/managed/managed_bookmark_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/mac/menu_text_elider_mac.h"
#include "ui/resources/grit/ui_resources.h"

using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;

namespace {

// Recursively clear any delegates from |menu| and its unbuilt submenus.
void ClearDelegatesFromSubmenu(NSMenu* menu) {
  CHECK(menu);
  // Either the delegate has been cleared, or items were never added.
  CHECK(![menu delegate] || [menu numberOfItems] == 0);
  [menu setDelegate:nil];
  NSArray* items = [menu itemArray];
  for (NSMenuItem* item in items) {
    if ([item hasSubmenu]) {
      ClearDelegatesFromSubmenu([item submenu]);
    }
  }
}

NSString* MenuTitleForNode(const BookmarkNode* node) {
  return base::SysUTF16ToNSString(gfx::ElideMenuItemTitle(node->GetTitle()));
}

}  // namespace

BookmarkMenuBridge::BookmarkMenuBridge(Profile* profile, NSMenu* menu_root)
    : profile_(profile),
      controller_([[BookmarkMenuCocoaController alloc] initWithBridge:this]),
      menu_root_(menu_root) {
  CHECK(profile_);
  profile_dir_ = profile->GetPath();
  CHECK(menu_root_);
  CHECK(![menu_root_ delegate]);
  [menu_root_ setDelegate:controller_];

  bookmark_service_ =
      BookmarkMergedSurfaceServiceFactory::GetForProfile(profile_);

  // The bookmark service is only availble for Regular and Guest profiles.
  if (!bookmark_service_) {
    return;
  }

  bookmark_service_observation_.Observe(bookmark_service_);
  if (bookmark_service_->loaded()) {
    BookmarkMergedSurfaceServiceLoaded();
  }
}

BookmarkMenuBridge::~BookmarkMenuBridge() {
  ClearBookmarkMenu();
  [menu_root_ setDelegate:nil];
}

void BookmarkMenuBridge::BookmarkMergedSurfaceServiceLoaded() {
  InvalidateMenu();
}

bool BookmarkMenuBridge::IsMenuRoot(NSMenu* menu) {
  CHECK(menu);
  return menu == menu_root_;
}

void BookmarkMenuBridge::UpdateRootMenuIfInvalid() {
  CHECK(menu_root_);
  if (!IsMenuValid()) {
    BuildRootMenu(/*recurse=*/false);
  }
}

void BookmarkMenuBridge::UpdateNonRootMenu(NSMenu* menu,
                                           const BookmarkParentFolder& folder) {
  CHECK(menu);
  CHECK(!IsMenuRoot(menu));
  CHECK(controller_);
  CHECK_EQ([menu delegate], controller_);

  AddChildrenToMenu(folder, menu, /*recurse=*/false);

  // Clear the delegate to prevent further refreshes.
  [menu setDelegate:nil];
}

bool BookmarkMenuBridge::HasContent(const BookmarkParentFolder& folder) {
  // TODO(crbug.com/390398329): Verify if this should be replaced with
  // checking visibility of underlying nodes.
  return bookmark_service_->GetChildrenCount(folder) > 0;
}

void BookmarkMenuBridge::BuildRootMenu(bool recurse) {
  if (!bookmark_service_ || !bookmark_service_->loaded()) {
    return;
  }

  if (!folder_image_) {
    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    folder_image_ = rb.GetNativeImageNamed(IDR_FOLDER_CLOSED).ToNSImage();
    [folder_image_ setTemplate:YES];
  }

  ClearBookmarkMenu();

  BookmarkParentFolder bookmark_bar_folder =
      BookmarkParentFolder::BookmarkBarFolder();
  BookmarkParentFolder managed_folder = BookmarkParentFolder::ManagedFolder();

  // Add at most one separator for the bookmark bar and the managed bookmarks
  // folder.
  if (HasContent(bookmark_bar_folder) || HasContent(managed_folder)) {
    [menu_root_ addItem:[NSMenuItem separatorItem]];
  }

  if (HasContent(managed_folder)) {
    // Most users never see this node, so the image is only loaded if needed.
    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    NSImage* image =
        rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER_MANAGED).ToNSImage();
    AddSubmenu(menu_root_, managed_folder, image, recurse);
  }

  // Add entries from the bookmark bar to the root menu.
  for (const BookmarkNode* node :
       bookmark_service_->GetChildren(bookmark_bar_folder)) {
    AddNodeToMenu(node, menu_root_, recurse);
  }

  BookmarkParentFolder other_folder = BookmarkParentFolder::OtherFolder();
  BookmarkParentFolder mobile_folder = BookmarkParentFolder::MobileFolder();

  // Add at most one separator for the "Other Bookmarks" and "Mobile Bookmarks"
  // folders.
  if (HasContent(other_folder) || HasContent(mobile_folder)) {
    [menu_root_ addItem:[NSMenuItem separatorItem]];
  }

  if (HasContent(other_folder)) {
    AddSubmenu(menu_root_, other_folder, folder_image_, recurse);
  }
  if (HasContent(mobile_folder)) {
    AddSubmenu(menu_root_, mobile_folder, folder_image_, recurse);
  }

  is_menu_valid_ = true;
}

void BookmarkMenuBridge::BookmarkMergedSurfaceServiceBeingDeleted() {}

void BookmarkMenuBridge::BookmarkNodeAdded(const BookmarkParentFolder& parent,
                                           size_t index) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkNodesRemoved(
    const BookmarkParentFolder& parent,
    const base::flat_set<const bookmarks::BookmarkNode*>& nodes) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkNodeMoved(
    const BookmarkParentFolder& old_parent,
    size_t old_index,
    const BookmarkParentFolder& new_parent,
    size_t new_index) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkNodeChanged(const BookmarkNode* node) {
  NSMenuItem* item = MenuItemForNode(node);
  if (item) {
    ConfigureMenuItem(node, item);
  }
}

void BookmarkMenuBridge::BookmarkNodeFaviconChanged(const BookmarkNode* node) {
  NSMenuItem* item = MenuItemForNode(node);
  if (item) {
    ConfigureMenuItem(node, item);
  }
}

void BookmarkMenuBridge::BookmarkParentFolderChildrenReordered(
    const BookmarkParentFolder& folder) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkAllUserNodesRemoved() {
  InvalidateMenu();
}

BookmarkModel* BookmarkMenuBridge::GetBookmarkModelForTesting() {
  CHECK(bookmark_service_);
  return bookmark_service_->bookmark_model();
}

Profile* BookmarkMenuBridge::GetProfile() {
  return profile_;
}

const base::FilePath& BookmarkMenuBridge::GetProfileDir() const {
  return profile_dir_;
}

NSMenu* BookmarkMenuBridge::BookmarkMenu() {
  return menu_root_;
}

void BookmarkMenuBridge::ClearBookmarkMenu() {
  InvalidateMenu();
  bookmark_nodes_.clear();
  tag_to_guid_.clear();

  // Recursively delete all menus that look like a bookmark. Also delete all
  // separator items since we explicitly add them back in. This deletes
  // everything except the first item ("Add Bookmark...").
  NSArray* items = [menu_root_ itemArray];
  for (NSMenuItem* item in items) {
    // If there's a submenu, it may have a reference to |controller_|. Ensure
    // that gets nerfed recursively.
    if ([item hasSubmenu]) {
      ClearDelegatesFromSubmenu([item submenu]);
    }

    // Convention: items in the bookmark list which are bookmarks have
    // an action of openBookmarkMenuItem:.  Also, assume all items
    // with submenus are submenus of bookmarks.
    if (([item action] == @selector(openBookmarkMenuItem:)) ||
        [item hasSubmenu] || [item isSeparatorItem]) {
      // This will eventually [obj release] all its kids, if it has any.
      [menu_root_ removeItem:item];
    } else {
      // Leave it alone.
    }
  }
}

void BookmarkMenuBridge::AddSubmenu(NSMenu* menu,
                                    const BookmarkParentFolder& folder,
                                    NSImage* image,
                                    bool recurse) {
  // For permanent folders containing multiple nodes, use the first node's title
  // as the menu title.
  auto nodes = bookmark_service_->GetUnderlyingNodes(folder);
  CHECK(!nodes.empty());
  const BookmarkNode* node = nodes[0];
  NSString* title = MenuTitleForNode(node);
  NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title
                                                action:nil
                                         keyEquivalent:@""];
  [item setImage:image];
  ConfigureMenuItem(node, item);
  bookmark_nodes_[node] = item;

  NSMenu* submenu = [[NSMenu alloc] initWithTitle:title];
  [menu setSubmenu:submenu forItem:item];

  // Set a delegate and a tag on the item so that the submenu can be populated
  // when (and if) Cocoa asks for it.
  if (!recurse) {
    [submenu setDelegate:controller_];
  }

  [menu addItem:item];

  if (recurse) {
    AddChildrenToMenu(folder, submenu, recurse);
  }
}

void BookmarkMenuBridge::AddChildrenToMenu(const BookmarkParentFolder& folder,
                                           NSMenu* menu,
                                           bool recurse) {
  BookmarkParentFolderChildren children =
      bookmark_service_->GetChildren(folder);
  if (!children.size()) {
    // Permanent folders with no children are not visible.
    CHECK(folder.HoldsNonPermanentFolder());
    // For empty non-permanent folder, show an unclickable entry with the text
    // "(empty)".
    NSString* empty_string = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU);
    NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:empty_string
                                                  action:nil
                                           keyEquivalent:@""];
    [menu addItem:item];
    return;
  }
  for (const BookmarkNode* child : children) {
    AddNodeToMenu(child, menu, recurse);
  }
}

void BookmarkMenuBridge::AddNodeToMenu(const BookmarkNode* node,
                                       NSMenu* menu,
                                       bool recurse) {
  if (node->is_folder()) {
    AddSubmenu(menu, BookmarkParentFolder::FromFolderNode(node), folder_image_,
               recurse);
  } else {
    CHECK(node->is_url());
    NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:MenuTitleForNode(node)
                                                  action:nil
                                           keyEquivalent:@""];
    bookmark_nodes_[node] = item;
    ConfigureMenuItem(node, item);
    [menu addItem:item];
  }
}

void BookmarkMenuBridge::ConfigureMenuItem(const BookmarkNode* node,
                                           NSMenuItem* item) {
  [item setTitle:MenuTitleForNode(node)];
  [item setTag:node->id()];
  tag_to_guid_[node->id()] = node->uuid();

  // The following settings only apply to URL items.
  if (node->is_folder()) {
    return;
  }
  CHECK(node->is_url());

  [item setTarget:controller_];
  [item setAction:@selector(openBookmarkMenuItem:)];
  [item setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];

  // Check to see if we have a favicon.
  NSImage* favicon = nil;
  BookmarkModel* model = bookmark_service_->bookmark_model();
  if (model) {
    const gfx::Image& image = model->GetFavicon(node);
    if (!image.IsEmpty()) {
      favicon = image.ToNSImage();
    }
  }
  // If we do not have a loaded favicon, use the default site image instead.
  if (!favicon) {
    favicon = favicon::GetDefaultFavicon().ToNSImage();
    [favicon setTemplate:YES];
  }
  [item setImage:favicon];
}

NSMenuItem* BookmarkMenuBridge::MenuItemForNode(const BookmarkNode* node) {
  if (!node) {
    return nil;
  }
  auto it = bookmark_nodes_.find(node);
  if (it == bookmark_nodes_.end()) {
    return nil;
  }
  return it->second;
}

NSMenuItem* BookmarkMenuBridge::MenuItemForNodeForTest(
    const bookmarks::BookmarkNode* node) {
  return MenuItemForNode(node);
}

void BookmarkMenuBridge::OnProfileWillBeDestroyed() {
  // Recursively populate the menu before the bookmark service is destroyed.
  BuildRootMenu(/*recurse=*/true);

  bookmark_service_observation_.Reset();
  bookmark_service_ = nullptr;
  profile_ = nullptr;

  // |bookmark_nodes_| stores the nodes by pointer, so it would be unsafe to
  // keep them.
  bookmark_nodes_.clear();
}

base::Uuid BookmarkMenuBridge::TagToGUID(int64_t tag) const {
  const auto& it = tag_to_guid_.find(tag);
  return it == tag_to_guid_.end() ? base::Uuid() : it->second;
}
