// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/extensions/extension_bookmark_manager_api.h"

#include <vector>

#include "base/json/json_writer.h"
#include "base/string_number_conversions.h"
#include "base/values.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/bookmarks/bookmark_node_data.h"
#include "chrome/browser/bookmarks/bookmark_utils.h"
#include "chrome/browser/extensions/extension_bookmark_helpers.h"
#include "chrome/browser/extensions/extension_bookmarks_module_constants.h"
#include "chrome/browser/extensions/extension_event_router.h"
#include "chrome/browser/extensions/extension_web_ui.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/chrome_url_data_manager.h"
#include "content/browser/renderer_host/render_view_host.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"

namespace keys = extension_bookmarks_module_constants;

namespace {

// Returns a single bookmark node from the argument ID.
// This returns NULL in case of failure.
const BookmarkNode* GetNodeFromArguments(BookmarkModel* model,
    const ListValue* args) {
  std::string id_string;
  if (!args->GetString(0, &id_string))
    return NULL;
  int64 id;
  if (!base::StringToInt64(id_string, &id))
    return NULL;
  return model->GetNodeByID(id);
}

// Gets a vector of bookmark nodes from the argument list of IDs.
// This returns false in the case of failure.
bool GetNodesFromArguments(BookmarkModel* model, const ListValue* args,
    size_t args_index, std::vector<const BookmarkNode*>* nodes) {

  ListValue* ids;
  if (!args->GetList(args_index, &ids))
    return false;

  size_t count = ids->GetSize();
  if (count == 0)
    return false;

  for (size_t i = 0; i < count; ++i) {
    std::string id_string;
    if (!ids->GetString(i, &id_string))
      return false;
    int64 id;
    if (!base::StringToInt64(id_string, &id))
      return false;
    const BookmarkNode* node = model->GetNodeByID(id);
    if (!node)
      return false;
    nodes->push_back(node);
  }

  return true;
}

// Recursively adds a node to a list. This is by used |BookmarkNodeDataToJSON|
// when the data comes from the current profile. In this case we have a
// BookmarkNode since we got the data from the current profile.
void AddNodeToList(ListValue* list, const BookmarkNode& node) {
  DictionaryValue* dict = new DictionaryValue();

  // Add id and parentId so we can associate the data with existing nodes on the
  // client side.
  std::string id_string = base::Int64ToString(node.id());
  dict->SetString(keys::kIdKey, id_string);

  std::string parent_id_string = base::Int64ToString(node.parent()->id());
  dict->SetString(keys::kParentIdKey, parent_id_string);

  if (node.is_url())
    dict->SetString(keys::kUrlKey, node.GetURL().spec());

  dict->SetString(keys::kTitleKey, node.GetTitle());

  ListValue* children = new ListValue();
  for (int i = 0; i < node.child_count(); ++i)
    AddNodeToList(children, *node.GetChild(i));
  dict->Set(keys::kChildrenKey, children);

  list->Append(dict);
}

// Recursively adds an element to a list. This is used by
// |BookmarkNodeDataToJSON| when the data comes from a different profile. When
// the data comes from a different profile we do not have any IDs or parent IDs.
void AddElementToList(ListValue* list,
                      const BookmarkNodeData::Element& element) {
  DictionaryValue* dict = new DictionaryValue();

  if (element.is_url)
    dict->SetString(keys::kUrlKey, element.url.spec());

  dict->SetString(keys::kTitleKey, element.title);

  ListValue* children = new ListValue();
  for (size_t i = 0; i < element.children.size(); ++i)
    AddElementToList(children, element.children[i]);
  dict->Set(keys::kChildrenKey, children);

  list->Append(dict);
}

// Builds the JSON structure based on the BookmarksDragData.
void BookmarkNodeDataToJSON(Profile* profile, const BookmarkNodeData& data,
                            ListValue* args) {
  bool same_profile = data.IsFromProfile(profile);
  DictionaryValue* value = new DictionaryValue();
  value->SetBoolean(keys::kSameProfileKey, same_profile);

  ListValue* list = new ListValue();
  if (same_profile) {
    std::vector<const BookmarkNode*> nodes = data.GetNodes(profile);
    for (size_t i = 0; i < nodes.size(); ++i)
      AddNodeToList(list, *nodes[i]);
  } else {
    // We do not have an node IDs when the data comes from a different profile.
    std::vector<BookmarkNodeData::Element> elements = data.elements;
    for (size_t i = 0; i < elements.size(); ++i)
      AddElementToList(list, elements[i]);
  }
  value->Set(keys::kElementsKey, list);

  args->Append(value);
}

}  // namespace

ExtensionBookmarkManagerEventRouter::ExtensionBookmarkManagerEventRouter(
    Profile* profile, TabContents* tab_contents)
    : profile_(profile),
    tab_contents_(tab_contents) {
  tab_contents_->SetBookmarkDragDelegate(this);
}

ExtensionBookmarkManagerEventRouter::~ExtensionBookmarkManagerEventRouter() {
  if (tab_contents_->GetBookmarkDragDelegate() == this)
    tab_contents_->SetBookmarkDragDelegate(NULL);
}

void ExtensionBookmarkManagerEventRouter::DispatchEvent(const char* event_name,
                                                        const ListValue* args) {
  if (!profile_->GetExtensionEventRouter())
    return;

  std::string json_args;
  base::JSONWriter::Write(args, false, &json_args);
  profile_->GetExtensionEventRouter()->DispatchEventToRenderers(
      event_name, json_args, NULL, GURL());
}

void ExtensionBookmarkManagerEventRouter::DispatchDragEvent(
    const BookmarkNodeData& data, const char* event_name) {
  if (data.size() == 0)
    return;

  ListValue args;
  BookmarkNodeDataToJSON(profile_, data, &args);
  DispatchEvent(event_name, &args);
}

void ExtensionBookmarkManagerEventRouter::OnDragEnter(
    const BookmarkNodeData& data) {
  DispatchDragEvent(data, keys::kOnBookmarkDragEnter);
}

void ExtensionBookmarkManagerEventRouter::OnDragOver(
    const BookmarkNodeData& data) {
  // Intentionally empty since these events happens too often and floods the
  // message queue. We do not need this event for the bookmark manager anyway.
}

void ExtensionBookmarkManagerEventRouter::OnDragLeave(
    const BookmarkNodeData& data) {
  DispatchDragEvent(data, keys::kOnBookmarkDragLeave);
}

void ExtensionBookmarkManagerEventRouter::OnDrop(
    const BookmarkNodeData& data) {
  DispatchDragEvent(data, keys::kOnBookmarkDrop);

  // Make a copy that is owned by this instance.
  ClearBookmarkNodeData();
  bookmark_drag_data_ = data;
}

const BookmarkNodeData*
ExtensionBookmarkManagerEventRouter::GetBookmarkNodeData() {
  if (bookmark_drag_data_.is_valid())
    return &bookmark_drag_data_;
  return NULL;
}

void ExtensionBookmarkManagerEventRouter::ClearBookmarkNodeData() {
  bookmark_drag_data_.Clear();
}

bool ClipboardBookmarkManagerFunction::CopyOrCut(bool cut) {
  BookmarkModel* model = profile()->GetBookmarkModel();
  std::vector<const BookmarkNode*> nodes;
  EXTENSION_FUNCTION_VALIDATE(GetNodesFromArguments(model, args_.get(),
                                                    0, &nodes));
  bookmark_utils::CopyToClipboard(model, nodes, cut);
  return true;
}

bool CopyBookmarkManagerFunction::RunImpl() {
  return CopyOrCut(false);
}

bool CutBookmarkManagerFunction::RunImpl() {
  if (!EditBookmarksEnabled())
    return false;
  return CopyOrCut(true);
}

bool PasteBookmarkManagerFunction::RunImpl() {
  if (!EditBookmarksEnabled())
    return false;
  BookmarkModel* model = profile()->GetBookmarkModel();
  const BookmarkNode* parent_node = GetNodeFromArguments(model, args_.get());
  if (!parent_node) {
    error_ = keys::kNoParentError;
    return false;
  }
  bool can_paste = bookmark_utils::CanPasteFromClipboard(parent_node);
  if (!can_paste)
    return false;

  // We want to use the highest index of the selected nodes as a destination.
  std::vector<const BookmarkNode*> nodes;
  // No need to test return value, if we got an empty list, we insert at end.
  GetNodesFromArguments(model, args_.get(), 1, &nodes);
  int highest_index = -1;  // -1 means insert at end of list.
  for (size_t node = 0; node < nodes.size(); ++node) {
    // + 1 so that we insert after the selection.
    int this_node_index = parent_node->GetIndexOf(nodes[node]) + 1;
    if (this_node_index > highest_index)
      highest_index = this_node_index;
  }

  bookmark_utils::PasteFromClipboard(model, parent_node, highest_index);
  return true;
}

bool CanPasteBookmarkManagerFunction::RunImpl() {
  if (!EditBookmarksEnabled())
    return false;
  BookmarkModel* model = profile()->GetBookmarkModel();
  const BookmarkNode* parent_node = GetNodeFromArguments(model, args_.get());
  if (!parent_node) {
    error_ = keys::kNoParentError;
    return false;
  }
  bool can_paste = bookmark_utils::CanPasteFromClipboard(parent_node);
  result_.reset(Value::CreateBooleanValue(can_paste));
  SendResponse(true);
  return true;
}

bool SortChildrenBookmarkManagerFunction::RunImpl() {
  if (!EditBookmarksEnabled())
    return false;
  BookmarkModel* model = profile()->GetBookmarkModel();
  const BookmarkNode* parent_node = GetNodeFromArguments(model, args_.get());
  if (!parent_node) {
    error_ = keys::kNoParentError;
    return false;
  }
  model->SortChildren(parent_node);
  return true;
}

bool BookmarkManagerGetStringsFunction::RunImpl() {
  DictionaryValue* localized_strings = new DictionaryValue();

  localized_strings->SetString("title",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_TITLE));
  localized_strings->SetString("search_button",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_SEARCH_BUTTON));
  localized_strings->SetString("show_in_folder",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_SHOW_IN_FOLDER));
  localized_strings->SetString("sort",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_SORT));
  localized_strings->SetString("organize_menu",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_ORGANIZE_MENU));
  localized_strings->SetString("tools_menu",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_TOOLS_MENU));
  localized_strings->SetString("import_menu",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_IMPORT_MENU));
  localized_strings->SetString("export_menu",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_EXPORT_MENU));
  localized_strings->SetString("rename_folder",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_RENAME_FOLDER));
  localized_strings->SetString("edit",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_EDIT));
  localized_strings->SetString("should_open_all",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_SHOULD_OPEN_ALL));
  localized_strings->SetString("open_incognito",
      l10n_util::GetStringUTF16(IDS_BOOMARK_BAR_OPEN_INCOGNITO));
  localized_strings->SetString("open_in_new_tab",
      l10n_util::GetStringUTF16(IDS_BOOMARK_BAR_OPEN_IN_NEW_TAB));
  localized_strings->SetString("open_in_new_window",
      l10n_util::GetStringUTF16(IDS_BOOMARK_BAR_OPEN_IN_NEW_WINDOW));
  localized_strings->SetString("add_new_bookmark",
      l10n_util::GetStringUTF16(IDS_BOOMARK_BAR_ADD_NEW_BOOKMARK));
  localized_strings->SetString("new_folder",
      l10n_util::GetStringUTF16(IDS_BOOMARK_BAR_NEW_FOLDER));
  localized_strings->SetString("open_all",
      l10n_util::GetStringUTF16(IDS_BOOMARK_BAR_OPEN_ALL));
  localized_strings->SetString("open_all_new_window",
      l10n_util::GetStringUTF16(IDS_BOOMARK_BAR_OPEN_ALL_NEW_WINDOW));
  localized_strings->SetString("open_all_incognito",
      l10n_util::GetStringUTF16(IDS_BOOMARK_BAR_OPEN_ALL_INCOGNITO));
  localized_strings->SetString("remove",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_BAR_REMOVE));
  localized_strings->SetString("copy",
      l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_COPY));
  localized_strings->SetString("cut",
      l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_CUT));
  localized_strings->SetString("paste",
      l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_PASTE));
  localized_strings->SetString("delete",
      l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_DELETE));
  localized_strings->SetString("new_folder_name",
      l10n_util::GetStringUTF16(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME));
  localized_strings->SetString("name_input_placeholder",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_NAME_INPUT_PLACE_HOLDER));
  localized_strings->SetString("url_input_placeholder",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_URL_INPUT_PLACE_HOLDER));
  localized_strings->SetString("invalid_url",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_INVALID_URL));
  localized_strings->SetString("recent",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_RECENT));
  localized_strings->SetString("search",
      l10n_util::GetStringUTF16(IDS_BOOKMARK_MANAGER_SEARCH));

  ChromeURLDataManager::DataSource::SetFontAndTextDirection(localized_strings);

  result_.reset(localized_strings);
  SendResponse(true);
  return true;
}

bool StartDragBookmarkManagerFunction::RunImpl() {
  if (!EditBookmarksEnabled())
    return false;
  BookmarkModel* model = profile()->GetBookmarkModel();
  std::vector<const BookmarkNode*> nodes;
  EXTENSION_FUNCTION_VALIDATE(
      GetNodesFromArguments(model, args_.get(), 0, &nodes));

  if (dispatcher()->render_view_host()->delegate()->GetRenderViewType() ==
      ViewType::TAB_CONTENTS) {
    ExtensionWebUI* web_ui =
        static_cast<ExtensionWebUI*>(dispatcher()->delegate());
    bookmark_utils::DragBookmarks(
        profile(), nodes, web_ui->tab_contents()->GetNativeView());

    return true;
  } else {
    NOTREACHED();
    return false;
  }
}

bool DropBookmarkManagerFunction::RunImpl() {
  if (!EditBookmarksEnabled())
    return false;

  BookmarkModel* model = profile()->GetBookmarkModel();

  int64 id;
  std::string id_string;
  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &id_string));

  if (!base::StringToInt64(id_string, &id)) {
    error_ = keys::kInvalidIdError;
    return false;
  }

  const BookmarkNode* drop_parent = model->GetNodeByID(id);
  if (!drop_parent) {
    error_ = keys::kNoParentError;
    return false;
  }

  int drop_index;
  if (args_->GetSize() == 2)
    EXTENSION_FUNCTION_VALIDATE(args_->GetInteger(1, &drop_index));
  else
    drop_index = drop_parent->child_count();

  if (dispatcher()->render_view_host()->delegate()->GetRenderViewType() ==
      ViewType::TAB_CONTENTS) {
    ExtensionWebUI* web_ui =
        static_cast<ExtensionWebUI*>(dispatcher()->delegate());
    ExtensionBookmarkManagerEventRouter* router =
        web_ui->extension_bookmark_manager_event_router();

    DCHECK(router);
    const BookmarkNodeData* drag_data = router->GetBookmarkNodeData();
    if (drag_data == NULL) {
      NOTREACHED() <<"Somehow we're dropping null bookmark data";
      return false;
    }
    bookmark_utils::PerformBookmarkDrop(profile(),
                                        *drag_data,
                                        drop_parent, drop_index);

    router->ClearBookmarkNodeData();
    SendResponse(true);
    return true;
  } else {
    NOTREACHED();
    return false;
  }
}

bool GetSubtreeBookmarkManagerFunction::RunImpl() {
  BookmarkModel* model = profile()->GetBookmarkModel();
  const BookmarkNode* node;
  int64 id;
  std::string id_string;
  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &id_string));
  bool folders_only;
  EXTENSION_FUNCTION_VALIDATE(args_->GetBoolean(1, &folders_only));
  if (id_string == "") {
    node = model->root_node();
  } else {
     if (!base::StringToInt64(id_string, &id)) {
      error_ = keys::kInvalidIdError;
      return false;
    }
    node = model->GetNodeByID(id);
  }
  if (!node) {
    error_ = keys::kNoNodeError;
    return false;
  }
  scoped_ptr<ListValue> json(new ListValue());
  if (folders_only) {
    extension_bookmark_helpers::AddNodeFoldersOnly(node,
                                                   json.get(),
                                                   true);
  } else {
    extension_bookmark_helpers::AddNode(node, json.get(), true);
  }
  result_.reset(json.release());
  return true;
}