// Copyright 2013 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_editor.h"

#include <stddef.h>

#include "base/containers/flat_set.h"
#include "chrome/grit/generated_resources.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/common/bookmark_metrics.h"
#include "components/strings/grit/components_strings.h"

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

namespace {

const BookmarkNode* CreateNewNode(BookmarkModel* model,
                                  const BookmarkNode* parent,
                                  const BookmarkEditor::EditDetails& details,
                                  const std::u16string& new_title,
                                  const GURL& new_url) {
  const BookmarkNode* node = nullptr;
  // When create the new one to right-clicked folder, add it to the next to the
  // folder's position. Because |details.index| has a index of the folder when
  // it was right-clicked, it might cause out of range exception when another
  // bookmark manager edits contents of the folder.
  // So we must check the range.
  size_t child_count = parent->children().size();
  size_t insert_index =
      (parent == details.parent_node && details.index.has_value() &&
       details.index.value() <= child_count)
          ? details.index.value()
          : child_count;
  switch (details.type) {
    case BookmarkEditor::EditDetails::NEW_URL:
      node = model->AddNewURL(parent, insert_index, new_title, new_url);
      break;
    case BookmarkEditor::EditDetails::NEW_FOLDER:
    case BookmarkEditor::EditDetails::CONVERT_TAB_GROUP_TO_FOLDER: {
      node = model->AddFolder(parent, insert_index, new_title);
      for (const auto& bookmark_data : details.bookmark_data.children) {
        if (bookmark_data.url.has_value()) {
          model->AddURL(node, node->children().size(), bookmark_data.title,
                        bookmark_data.url.value());
        } else {
          const BookmarkNode* nested_node = model->AddFolder(
              node, node->children().size(), bookmark_data.title);
          for (auto& child : bookmark_data.children) {
            // We do not expect to create new folders more than 2 levels deep.
            DCHECK(child.url.has_value());
            model->AddURL(nested_node, nested_node->children().size(),
                          child.title, child.url.value());
          }
          model->SetDateFolderModified(node, base::Time::Now());
        }
      }
      model->SetDateFolderModified(parent, base::Time::Now());

      break;
    }
    case BookmarkEditor::EditDetails::EXISTING_NODE:
    case BookmarkEditor::EditDetails::MOVE:
      NOTREACHED();
  }

  return node;
}

const BookmarkNode* GetParentNodeForMove(
    bookmarks::BookmarkModel* model,
    const std::vector<
        raw_ptr<const bookmarks::BookmarkNode, VectorExperimental>>& nodes) {
  CHECK(!nodes.empty());
  const BookmarkNode* first_parent = nodes[0].get()->parent();

  bool same_parent = std::ranges::all_of(nodes, [&first_parent](auto node) {
    return node->parent() == first_parent;
  });

  // Default to the parent node if all of the nodes have the same parent.
  if (same_parent) {
    return first_parent;
  }

  // If the nodes do not have the same parent, but at least one of them is
  // saved to account storage, default to the account other node.
  if (model->account_other_node()) {
    bool only_local_nodes = std::ranges::all_of(
        nodes, [&model](auto node) { return model->IsLocalOnlyNode(*node); });

    if (!only_local_nodes) {
      return model->account_other_node();
    }
  }

  // If the nodes are all saved to local storage or sync is enabled, default
  // to the local other node.
  return model->other_node();
}

}  // namespace

BookmarkEditor::EditDetails::BookmarkData::BookmarkData() = default;

BookmarkEditor::EditDetails::BookmarkData::BookmarkData(
    BookmarkData const& other) = default;

BookmarkEditor::EditDetails::BookmarkData::~BookmarkData() = default;

BookmarkEditor::EditDetails::EditDetails(Type node_type) : type(node_type) {}

bool BookmarkEditor::EditDetails::CanChangeUrl() const {
  switch (type) {
    case EXISTING_NODE:
      return existing_node->type() == BookmarkNode::URL;
    case NEW_URL:
      return true;
    case NEW_FOLDER:
    case CONVERT_TAB_GROUP_TO_FOLDER:
    case MOVE:
      return false;
  }
}

int BookmarkEditor::EditDetails::GetWindowTitleId() const {
  int dialog_title = IDS_BOOKMARK_EDITOR_TITLE;
  switch (type) {
    case EditDetails::EXISTING_NODE:
    case EditDetails::NEW_URL:
      dialog_title = (type == EditDetails::EXISTING_NODE &&
                      existing_node->type() == BookmarkNode::FOLDER)
                         ? IDS_BOOKMARK_FOLDER_EDITOR_WINDOW_TITLE
                         : IDS_BOOKMARK_EDITOR_TITLE;
      break;
    case EditDetails::NEW_FOLDER:
      dialog_title = bookmark_data.children.empty()
                         ? IDS_BOOKMARK_FOLDER_EDITOR_WINDOW_TITLE_NEW
                         : IDS_BOOKMARK_ALL_TABS_DIALOG_TITLE;
      break;
    case EditDetails::CONVERT_TAB_GROUP_TO_FOLDER:
      dialog_title = IDS_TAB_GROUP_TO_BOOKMARK_FOLDER_TITLE;
      break;
    case EditDetails::MOVE:
      dialog_title = IDS_BOOKMARK_MOVE_DIALOG_TITLE;
      break;
    default:
      NOTREACHED();
  }
  return dialog_title;
}

BookmarkEditor::EditDetails BookmarkEditor::EditDetails::EditNode(
    const BookmarkNode* node) {
  EditDetails details(EXISTING_NODE);
  details.existing_node = node;
  if (node) {
    details.parent_node = node->parent();
  }
  return details;
}

BookmarkEditor::EditDetails BookmarkEditor::EditDetails::AddNodeInFolder(
    const BookmarkNode* parent_node,
    size_t index,
    const GURL& url,
    const std::u16string& title) {
  EditDetails details(NEW_URL);
  details.parent_node = parent_node;
  details.index = index;
  details.bookmark_data.url = url;
  details.bookmark_data.title = title;
  return details;
}

BookmarkEditor::EditDetails BookmarkEditor::EditDetails::AddFolder(
    const BookmarkNode* parent_node,
    size_t index) {
  EditDetails details(NEW_FOLDER);
  details.parent_node = parent_node;
  details.index = index;
  return details;
}

BookmarkEditor::EditDetails BookmarkEditor::EditDetails::TabGroupToFolder(
    const BookmarkNode* parent_node,
    size_t index,
    const std::u16string& title) {
  EditDetails details(CONVERT_TAB_GROUP_TO_FOLDER);
  details.parent_node = parent_node;
  details.index = index;
  details.bookmark_data.title = title;
  return details;
}

BookmarkEditor::EditDetails BookmarkEditor::EditDetails::MoveNodes(
    bookmarks::BookmarkModel* model,
    const std::vector<
        raw_ptr<const bookmarks::BookmarkNode, VectorExperimental>>& nodes) {
  EditDetails details(MOVE);

  details.existing_nodes_to_move = base::flat_set<
      raw_ptr<const bookmarks::BookmarkNode, VectorExperimental>>(nodes);

  details.parent_node = GetParentNodeForMove(model, nodes);

  return details;
}

BookmarkEditor::EditDetails::EditDetails(const EditDetails& other) = default;

BookmarkEditor::EditDetails::~EditDetails() = default;

// static
void BookmarkEditor::ApplyEdits(BookmarkModel* model,
                                const BookmarkNode* new_parent,
                                const EditDetails& details,
                                const std::u16string& new_title,
                                const GURL& new_url) {
  if (details.type == EditDetails::NEW_URL ||
      details.type == EditDetails::NEW_FOLDER ||
      details.type == EditDetails::CONVERT_TAB_GROUP_TO_FOLDER) {
    CreateNewNode(model, new_parent, details, new_title, new_url);
    return;
  }

  if (details.type == EditDetails::MOVE) {
    std::vector<raw_ptr<const bookmarks::BookmarkNode, VectorExperimental>>
        nodes(details.existing_nodes_to_move.begin(),
              details.existing_nodes_to_move.end());

    // Persist the nodes' order within the same parent folder in the new
    // location. Between different parent folders, choose the order depending on
    // when they were last modified. More recently modified folders are rowed
    // last, as new bookmarks are usually added to the back of a folder.
    std::sort(
        nodes.begin(), nodes.end(),
        [](const bookmarks::BookmarkNode* l, const bookmarks::BookmarkNode* r) {
          if (l->parent() != r->parent()) {
            return l->parent()->date_folder_modified() <
                   r->parent()->date_folder_modified();
          }
          return l->parent()->GetIndexOf(l) < r->parent()->GetIndexOf(r);
        });

    for (const bookmarks::BookmarkNode* node_to_move : nodes) {
      model->Move(node_to_move, new_parent, new_parent->children().size());
    }
    return;
  }

  const BookmarkNode* node = details.existing_node;
  DCHECK(node);

  if (new_parent != node->parent()) {
    model->Move(node, new_parent, new_parent->children().size());
  }
  if (node->is_url()) {
    model->SetURL(node, new_url, bookmarks::metrics::BookmarkEditSource::kUser);
  }

  model->SetTitle(node, new_title,
                  bookmarks::metrics::BookmarkEditSource::kUser);
}
