// Copyright (c) 2006-2008 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/views/bookmark_folder_tree_view.h"

#include <commctrl.h>

#include "app/drag_drop_types.h"
#include "app/os_exchange_data.h"
#include "app/os_exchange_data_provider_win.h"
#include "base/base_drag_source.h"
#include "chrome/browser/bookmarks/bookmark_utils.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/bookmarks/bookmark_folder_tree_model.h"
#include "chrome/browser/profile.h"
#include "grit/generated_resources.h"
#include "views/view_constants.h"

void BookmarkFolderTreeView::DropInfo::Scrolled() {
  view_->UpdateDropInfo();
}

BookmarkFolderTreeView::BookmarkFolderTreeView(Profile* profile,
                                               BookmarkFolderTreeModel* model)
    : views::TreeView(),
      profile_(profile),
      is_dragging_(false) {
  SetModel(model);
  SetEditable(false);
  SetRootShown(false);
  set_drag_enabled(true);
}

bool BookmarkFolderTreeView::CanDrop(const OSExchangeData& data) {
  if (!profile_->GetBookmarkModel()->IsLoaded())
    return false;

  BookmarkDragData drag_data;

  if (!drag_data.Read(data))
    return false;

  drop_info_.reset(new DropInfo(this));
  drop_info_->SetData(drag_data);

  // See if there are any urls being dropped.
  for (size_t i = 0; i < drop_info_->data().size(); ++i) {
    if (drop_info_->data().elements[0].is_url) {
      drop_info_->set_only_folders(false);
      break;
    }
  }

  return true;
}

void BookmarkFolderTreeView::OnDragEntered(
    const views::DropTargetEvent& event) {
}

int BookmarkFolderTreeView::OnDragUpdated(const views::DropTargetEvent& event) {
  drop_info_->Update(event);
  return UpdateDropInfo();
}

void BookmarkFolderTreeView::OnDragExited() {
  SetDropPosition(DropPosition());

  drop_info_.reset();
}

int BookmarkFolderTreeView::OnPerformDrop(const views::DropTargetEvent& event) {
  OnPerformDropImpl();

  int drop_operation = drop_info_->drop_operation();
  SetDropPosition(DropPosition());
  drop_info_.reset();
  return drop_operation;
}

const BookmarkNode* BookmarkFolderTreeView::GetSelectedBookmarkNode() {
  TreeModelNode* selected_node = GetSelectedNode();
  if (!selected_node)
    return NULL;
  return TreeNodeAsBookmarkNode(folder_model()->AsNode(selected_node));
}

LRESULT BookmarkFolderTreeView::OnNotify(int w_param, LPNMHDR l_param) {
  switch (l_param->code) {
    case TVN_BEGINDRAG: {
      HTREEITEM tree_item =
          reinterpret_cast<LPNMTREEVIEW>(l_param)->itemNew.hItem;
      FolderNode* folder_node =
          folder_model()->AsNode(GetNodeForTreeItem(tree_item));
      BeginDrag(TreeNodeAsBookmarkNode(folder_node));
      return 0;  // Return value ignored.
    }
  }

  return TreeView::OnNotify(w_param, l_param);
}

int BookmarkFolderTreeView::UpdateDropInfo() {
  DropPosition position =
      CalculateDropPosition(drop_info_->last_y(), drop_info_->only_folders());
  drop_info_->set_drop_operation(CalculateDropOperation(position));

  if (drop_info_->drop_operation() == DragDropTypes::DRAG_NONE)
    position = DropPosition();

  SetDropPosition(position);

  return drop_info_->drop_operation();
}

void BookmarkFolderTreeView::BeginDrag(const BookmarkNode* node) {
  BookmarkModel* model = profile_->GetBookmarkModel();
  // Only allow the drag if the user has selected a node of type bookmark and it
  // isn't the bookmark bar or other bookmarks folders.
  if (!node || node == model->other_node() ||
      node == model->GetBookmarkBarNode()) {
    return;
  }

  std::vector<const BookmarkNode*> nodes_to_drag;
  nodes_to_drag.push_back(node);

  OSExchangeData data;
  BookmarkDragData(nodes_to_drag).Write(profile_, &data);
  scoped_refptr<BaseDragSource> drag_source(new BaseDragSource);
  DWORD effects;
  is_dragging_ = true;
  DoDragDrop(OSExchangeDataProviderWin::GetIDataObject(data), drag_source,
             DROPEFFECT_LINK | DROPEFFECT_COPY | DROPEFFECT_MOVE, &effects);
  is_dragging_ = false;
}

BookmarkFolderTreeView::DropPosition BookmarkFolderTreeView::
    CalculateDropPosition(int y, bool only_folders) {
  HWND hwnd = GetNativeControlHWND();
  HTREEITEM item = TreeView_GetFirstVisible(hwnd);
  while (item) {
    RECT bounds;
    TreeView_GetItemRect(hwnd, item, &bounds, TRUE);
    if (y < bounds.bottom) {
      TreeModelNode* model_node = GetNodeForTreeItem(item);
      if (folder_model()->GetNodeType(model_node) !=
          BookmarkFolderTreeModel::BOOKMARK) {
        // Only allow drops on bookmark nodes.
        return DropPosition();
      }

      FolderNode* node = folder_model()->AsNode(model_node);
      if (!only_folders || !node->GetParent() ||
          !node->GetParent()->GetParent()) {
        // If some of the elements being dropped are urls, then we only allow
        // dropping on a folder. Similarly you can't drop between the
        // bookmark bar and other folder nodes.
        return DropPosition(node, node->GetChildCount(), true);
      }

      // Drop contains all folders, allow them to be dropped between
      // folders.
      if (y < bounds.top + views::kDropBetweenPixels) {
        return DropPosition(node->GetParent(),
                            node->GetParent()->IndexOfChild(node), false);
      }
      if (y >= bounds.bottom - views::kDropBetweenPixels) {
        if (IsExpanded(node) && folder_model()->GetChildCount(node) > 0) {
          // The node is expanded and has children, treat the drop as occurring
          // as the first child. This is done to avoid the selection highlight
          // dancing around when dragging over expanded folders. Without this
          // the highlight jumps past the last expanded child of node.
          return DropPosition(node, 0, false);
        }
        return DropPosition(node->GetParent(),
                            node->GetParent()->IndexOfChild(node) + 1, false);
      }
      return DropPosition(node, node->GetChildCount(), true);
    }
    item = TreeView_GetNextVisible(hwnd, item);
  }
  return DropPosition();
}

int BookmarkFolderTreeView::CalculateDropOperation(
    const DropPosition& position) {
  if (!position.parent)
    return DragDropTypes::DRAG_NONE;

  if (drop_info_->data().IsFromProfile(profile_)) {
    int bookmark_model_drop_index = FolderIndexToBookmarkIndex(position);
    if (!bookmark_utils::IsValidDropLocation(
            profile_, drop_info_->data(),
            TreeNodeAsBookmarkNode(position.parent),
            bookmark_model_drop_index)) {
      return DragDropTypes::DRAG_NONE;
    }

    // Data from the same profile. Prefer move, but do copy if the user wants
    // that.
    if (drop_info_->is_control_down())
      return DragDropTypes::DRAG_COPY;

    return DragDropTypes::DRAG_MOVE;
  }
  // We're going to copy, but return an operation compatible with the source
  // operations so that the user can drop.
  return bookmark_utils::PreferredDropOperation(
      drop_info_->source_operations(),
      DragDropTypes::DRAG_COPY | DragDropTypes::DRAG_LINK);
}

void BookmarkFolderTreeView::OnPerformDropImpl() {
  const BookmarkNode* parent_node =
      TreeNodeAsBookmarkNode(drop_info_->position().parent);
  int drop_index = FolderIndexToBookmarkIndex(drop_info_->position());
  BookmarkModel* model = profile_->GetBookmarkModel();
  // If the data is not from this profile we return an operation compatible
  // with the source. As such, we need to need to check the data here too.
  if (!drop_info_->data().IsFromProfile(profile_) ||
      drop_info_->drop_operation() == DragDropTypes::DRAG_COPY) {
    bookmark_utils::CloneDragData(model, drop_info_->data().elements,
                                  parent_node, drop_index);
    return;
  }

  // else, move.
  std::vector<const BookmarkNode*> nodes =
      drop_info_->data().GetNodes(profile_);
  if (nodes.empty())
    return;

  for (size_t i = 0; i < nodes.size(); ++i) {
    model->Move(nodes[i], parent_node, drop_index);
    // Reset the drop_index, just in case the index didn't really change.
    drop_index = parent_node->IndexOfChild(nodes[i]) + 1;
    if (nodes[i]->is_folder()) {
      Expand(folder_model()->GetFolderNodeForBookmarkNode(nodes[i]));
    }
  }

  if (is_dragging_ && nodes[0]->is_folder()) {
    // We're the drag source. Select the node that was moved.
    SetSelectedNode(folder_model()->GetFolderNodeForBookmarkNode(nodes[0]));
  }
}

void BookmarkFolderTreeView::SetDropPosition(const DropPosition& position) {
  if (drop_info_->position().equals(position))
    return;

  // Remove the indicator over the previous location.
  if (drop_info_->position().on) {
    HTREEITEM item = GetTreeItemForNode(drop_info_->position().parent);
    if (item)
      TreeView_SetItemState(GetNativeControlHWND(), item, 0, TVIS_DROPHILITED);
  } else if (drop_info_->position().index != -1) {
    TreeView_SetInsertMark(GetNativeControlHWND(), NULL, FALSE);
  }

  drop_info_->set_position(position);

  // And show the new indicator.
  if (position.on) {
    HTREEITEM item = GetTreeItemForNode(position.parent);
    if (item) {
      TreeView_SetItemState(GetNativeControlHWND(), item, TVIS_DROPHILITED,
                            TVIS_DROPHILITED);
    }
  } else if (position.index != -1) {
    BOOL after = FALSE;
    FolderNode* node = position.parent;
    if (folder_model()->GetChildCount(position.parent) == position.index) {
      after = TRUE;
      node = folder_model()->GetChild(position.parent, position.index - 1);
    } else {
      node = folder_model()->GetChild(position.parent, position.index);
    }
    HTREEITEM item = GetTreeItemForNode(node);
    if (item)
      TreeView_SetInsertMark(GetNativeControlHWND(), item, after);
  }
}

BookmarkFolderTreeModel* BookmarkFolderTreeView::folder_model() const {
  return static_cast<BookmarkFolderTreeModel*>(model());
}

const BookmarkNode* BookmarkFolderTreeView::TreeNodeAsBookmarkNode(
    FolderNode* node) {
  return folder_model()->TreeNodeAsBookmarkNode(node);
}

int BookmarkFolderTreeView::FolderIndexToBookmarkIndex(
    const DropPosition& position) {
  const BookmarkNode* parent_node = TreeNodeAsBookmarkNode(position.parent);
  if (position.on || position.index == position.parent->GetChildCount())
    return parent_node->GetChildCount();

  if (position.index != 0) {
    return parent_node->IndexOfChild(
        TreeNodeAsBookmarkNode(position.parent->GetChild(position.index)));
  }

  return 0;
}