// 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_table_view.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_table_model.h"
#include "chrome/browser/profile.h"
#include "chrome/common/drag_drop_types.h"
#include "chrome/common/gfx/chrome_canvas.h"
#include "chrome/common/gfx/chrome_font.h"
#include "chrome/common/os_exchange_data.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/pref_service.h"
#include "chrome/common/resource_bundle.h"
#include "chrome/views/view_constants.h"

#include "generated_resources.h"

namespace {

// Height of the drop indicator used when dropping between rows.
const int kDropHighlightHeight = 2;

int GetWidthOfColumn(const std::vector<views::TableColumn>& columns,
                     const std::vector<int> widths,
                     int column_id) {
  for (size_t i = 0; i < columns.size(); ++i) {
    if (columns[i].id == column_id)
      return widths[i];
  }
  NOTREACHED();
  return -1;
}

}  // namespace

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

BookmarkTableView::BookmarkTableView(Profile* profile,
                                     BookmarkTableModel* model)
    : views::TableView(model, std::vector<views::TableColumn>(),
                       views::ICON_AND_TEXT, false, true, true),
      profile_(profile),
      show_path_column_(false) {
  UpdateColumns();
}

// static
void BookmarkTableView::RegisterUserPrefs(PrefService* prefs) {
  prefs->RegisterIntegerPref(prefs::kBookmarkTableNameWidth1, -1);
  prefs->RegisterIntegerPref(prefs::kBookmarkTableURLWidth1, -1);
  prefs->RegisterIntegerPref(prefs::kBookmarkTableNameWidth2, -1);
  prefs->RegisterIntegerPref(prefs::kBookmarkTableURLWidth2, -1);
  prefs->RegisterIntegerPref(prefs::kBookmarkTablePathWidth, -1);
}

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

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

  // Don't allow the user to drop an ancestor of the parent node onto the
  // parent node. This would create a cycle, which is definitely a no-no.
  std::vector<BookmarkNode*> nodes = drag_data.GetNodes(profile_);
  for (size_t i = 0; i < nodes.size(); ++i) {
    if (parent_node_->HasAncestor(nodes[i]))
      return false;
  }

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

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

int BookmarkTableView::OnDragUpdated(const views::DropTargetEvent& event) {
  if (!parent_node_ || !drop_info_.get()) {
    drop_info_.reset(NULL);
    return false;
  }

  drop_info_->Update(event);
  return UpdateDropInfo();
}

void BookmarkTableView::OnDragExited() {
  SetDropPosition(DropPosition());
  drop_info_.reset();
}

int BookmarkTableView::OnPerformDrop(const views::DropTargetEvent& event) {
  OnPerformDropImpl();
  int drop_operation = drop_info_->drop_operation();
  SetDropPosition(DropPosition());
  drop_info_.reset();
  return drop_operation;
}

BookmarkTableModel* BookmarkTableView::bookmark_table_model() const {
  return static_cast<BookmarkTableModel*>(model());
}

void BookmarkTableView::SaveColumnConfiguration() {
  PrefService* prefs = profile_->GetPrefs();
  if (!prefs)
    return;

  if (show_path_column_) {
    prefs->SetInteger(prefs::kBookmarkTableNameWidth2,
                      GetColumnWidth(IDS_BOOKMARK_TABLE_TITLE));
    prefs->SetInteger(prefs::kBookmarkTableURLWidth2,
                      GetColumnWidth(IDS_BOOKMARK_TABLE_URL));
    prefs->SetInteger(prefs::kBookmarkTablePathWidth,
                      GetColumnWidth(IDS_BOOKMARK_TABLE_PATH));
  } else {
    prefs->SetInteger(prefs::kBookmarkTableNameWidth1,
                      GetColumnWidth(IDS_BOOKMARK_TABLE_TITLE));
    prefs->SetInteger(prefs::kBookmarkTableURLWidth1,
                      GetColumnWidth(IDS_BOOKMARK_TABLE_URL));
  }
}

void BookmarkTableView::SetAltText(const std::wstring& alt_text) {
  if (alt_text == alt_text_)
    return;

  alt_text_ = alt_text;
  if (!GetNativeControlHWND())
    return;

  RECT alt_text_bounds = GetAltTextBounds().ToRECT();
  InvalidateRect(GetNativeControlHWND(), &alt_text_bounds, FALSE);
}

void BookmarkTableView::SetShowPathColumn(bool show_path_column) {
  if (show_path_column == show_path_column_)
    return;

  SaveColumnConfiguration();

  show_path_column_ = show_path_column;
  UpdateColumns();
}

void BookmarkTableView::PostPaint() {
  PaintAltText();

  if (!drop_info_.get() || drop_info_->position().index == -1 ||
      drop_info_->position().on) {
    return;
  }

  RECT bounds = GetDropBetweenHighlightRect(drop_info_->position().index);
  HDC dc = GetDC(GetNativeControlHWND());
  HBRUSH brush = CreateSolidBrush(GetSysColor(COLOR_WINDOWTEXT));
  FillRect(dc, &bounds, brush);
  DeleteObject(brush);
  ReleaseDC(GetNativeControlHWND(), dc);
}

LRESULT BookmarkTableView::OnNotify(int w_param, LPNMHDR l_param) {
  switch(l_param->code) {
    case LVN_BEGINDRAG:
      BeginDrag();
      return 0;  // Return value doesn't matter for this message.
  }

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

int BookmarkTableView::UpdateDropInfo() {
  DropPosition position = CalculateDropPosition(drop_info_->last_y());

  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 BookmarkTableView::BeginDrag() {
  std::vector<BookmarkNode*> nodes_to_drag;
  for (TableView::iterator i = SelectionBegin(); i != SelectionEnd(); ++i)
    nodes_to_drag.push_back(bookmark_table_model()->GetNodeForRow(*i));
  if (nodes_to_drag.empty())
    return;  // Nothing to drag.

  // Reverse the nodes so that they are put on the clipboard in visual order.
  // We do this as SelectionBegin starts at the end of the visual order.
  std::reverse(nodes_to_drag.begin(), nodes_to_drag.end());

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

int BookmarkTableView::CalculateDropOperation(const DropPosition& position) {
  if (drop_info_->data().IsFromProfile(profile_)) {
    // 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;

    int real_drop_index;
    BookmarkNode* drop_parent = GetDropParentAndIndex(position,
                                                      &real_drop_index);
    if (!bookmark_utils::IsValidDropLocation(
        profile_, drop_info_->data(), drop_parent, real_drop_index)) {
      return DragDropTypes::DRAG_NONE;
    }
    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 BookmarkTableView::OnPerformDropImpl() {
  int drop_index;
  BookmarkNode* drop_parent = GetDropParentAndIndex(
      drop_info_->position(), &drop_index);
  BookmarkModel* model = profile_->GetBookmarkModel();
  int min_selection;
  int max_selection;
  // 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,
                                  drop_parent, drop_index);
    min_selection = drop_index;
    max_selection = drop_index +
        static_cast<int>(drop_info_->data().elements.size());
  } else {
    // else, move.
    std::vector<BookmarkNode*> nodes = drop_info_->data().GetNodes(profile_);
    if (nodes.empty())
      return;

    for (size_t i = 0; i < nodes.size(); ++i) {
      model->Move(nodes[i], drop_parent, drop_index);
      // Reset the drop_index, just in case the index didn't really change.
      drop_index = drop_parent->IndexOfChild(nodes[i]) + 1;
    }
    min_selection = drop_parent->IndexOfChild(nodes[0]);
    max_selection = min_selection + static_cast<int>(nodes.size());
  }
  if (drop_info_->position().on) {
    // The user dropped on a folder, select it.
    int index = parent_node_->IndexOfChild(drop_parent);
    if (index != -1)
      Select(index);
  } else if (min_selection < RowCount() && max_selection <= RowCount()) {
    // Select the moved/copied rows.
    Select(min_selection);
    if (min_selection + 1 < max_selection) {
      // SetSelectedState doesn't send notification, so we manually do it.
      for (int i = min_selection + 1; i < max_selection; ++i)
        SetSelectedState(i, true);
      if (observer())
        observer()->OnSelectionChanged();
    }
  }
}

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

  UpdateDropIndicator(drop_info_->position(), false);

  drop_info_->set_position(position);

  UpdateDropIndicator(drop_info_->position(), true);
}

void BookmarkTableView::UpdateDropIndicator(const DropPosition& position,
                                            bool turn_on) {
  if (position.index == -1)
    return;

  if (position.on) {
    ListView_SetItemState(GetNativeControlHWND(), position.index,
                          turn_on ? LVIS_DROPHILITED : 0, LVIS_DROPHILITED);
  } else {
    RECT bounds = GetDropBetweenHighlightRect(position.index);
    InvalidateRect(GetNativeControlHWND(), &bounds, FALSE);
  }
}

BookmarkTableView::DropPosition
    BookmarkTableView::CalculateDropPosition(int y) {
  HWND hwnd = GetNativeControlHWND();
  int row_count = RowCount();
  int top_index = ListView_GetTopIndex(hwnd);
  if (row_count == 0 || top_index < 0)
    return DropPosition(0, false);

  for (int i = top_index; i < row_count; ++i) {
    RECT bounds;
    ListView_GetItemRect(hwnd, i, &bounds, LVIR_BOUNDS);
    if (y < bounds.top)
      return DropPosition(i, false);
    if (y < bounds.bottom) {
      if (bookmark_table_model()->GetNodeForRow(i)->is_folder()) {
        if (y < bounds.top + views::kDropBetweenPixels)
          return DropPosition(i, false);
        if (y >= bounds.bottom - views::kDropBetweenPixels)
          return DropPosition(i + 1, false);
        return DropPosition(i, true);
      }
      if (y < (bounds.bottom - bounds.top) / 2 + bounds.top)
        return DropPosition(i, false);
      return DropPosition(i + 1, false);
    }
  }
  return DropPosition(row_count, false);
}

BookmarkNode* BookmarkTableView::GetDropParentAndIndex(
    const DropPosition& position,
    int* index) {
  if (position.on) {
    BookmarkNode* parent = parent_node_->GetChild(position.index);
    *index = parent->GetChildCount();
    return parent;
  }
  *index = position.index;
  return parent_node_;
}

RECT BookmarkTableView::GetDropBetweenHighlightRect(int index) {
  RECT bounds = { 0 };
  if (RowCount() == 0) {
    bounds.top = content_offset();
    bounds.left = 0;
    bounds.right = width();
  } else if (index >= RowCount()) {
    ListView_GetItemRect(GetNativeControlHWND(), index - 1, &bounds,
                         LVIR_BOUNDS);
    bounds.top = bounds.bottom - kDropHighlightHeight / 2;
  } else {
    ListView_GetItemRect(GetNativeControlHWND(), index, &bounds, LVIR_BOUNDS);
    bounds.top -= kDropHighlightHeight / 2;
  }
  bounds.bottom = bounds.top + kDropHighlightHeight;
  return bounds;
}

void BookmarkTableView::UpdateColumns() {
  PrefService* prefs = profile_->GetPrefs();
  views::TableColumn name_column =
      views::TableColumn(IDS_BOOKMARK_TABLE_TITLE, views::TableColumn::LEFT,
                         -1);
  views::TableColumn url_column =
      views::TableColumn(IDS_BOOKMARK_TABLE_URL, views::TableColumn::LEFT, -1);
  views::TableColumn path_column =
      views::TableColumn(IDS_BOOKMARK_TABLE_PATH, views::TableColumn::LEFT, -1);

  std::vector<views::TableColumn> columns;
  if (show_path_column_) {
    int name_width = -1;
    int url_width = -1;
    int path_width = -1;
    if (prefs) {
      name_width = prefs->GetInteger(prefs::kBookmarkTableNameWidth2);
      url_width = prefs->GetInteger(prefs::kBookmarkTableURLWidth2);
      path_width = prefs->GetInteger(prefs::kBookmarkTablePathWidth);
    }
    if (name_width != -1 && url_width != -1 && path_width != -1) {
      name_column.width = name_width;
      url_column.width = url_width;
      path_column.width = path_width;
    } else {
      name_column.percent = .5;
      url_column.percent = .25;
      path_column.percent= .25;
    }
    columns.push_back(name_column);
    columns.push_back(url_column);
    columns.push_back(path_column);
  } else {
    int name_width = -1;
    int url_width = -1;
    if (prefs) {
      name_width = prefs->GetInteger(prefs::kBookmarkTableNameWidth1);
      url_width = prefs->GetInteger(prefs::kBookmarkTableURLWidth1);
    }
    if (name_width != -1 && url_width != -1) {
      name_column.width = name_width;
      url_column.width = url_width;
    } else {
      name_column.percent = .5;
      url_column.percent = .5;
    }
    columns.push_back(name_column);
    columns.push_back(url_column);
  }
  SetColumns(columns);
  for (size_t i = 0; i < columns.size(); ++i)
    SetColumnVisibility(columns[i].id, true);
  OnModelChanged();
}

void BookmarkTableView::PaintAltText() {
  if (alt_text_.empty())
    return;

  HDC dc = GetDC(GetNativeControlHWND());
  ChromeFont font = GetAltTextFont();
  gfx::Rect bounds = GetAltTextBounds();
  ChromeCanvas canvas(bounds.width(), bounds.height(), false);
  canvas.DrawStringInt(alt_text_, font, SK_ColorDKGRAY, 0, 0, bounds.width(),
                       bounds.height());
  canvas.getTopPlatformDevice().drawToHDC(dc, bounds.x(), bounds.y(), NULL);
  ReleaseDC(GetNativeControlHWND(), dc);  
}

gfx::Rect BookmarkTableView::GetAltTextBounds() {
  static const int kXOffset = 16;
  DCHECK(GetNativeControlHWND());
  CRect client_rect;
  GetClientRect(GetNativeControlHWND(), client_rect);
  ChromeFont font = GetAltTextFont();
  return gfx::Rect(kXOffset, content_offset(), client_rect.Width() - kXOffset,
                   std::max(kImageSize, font.height()));
}

ChromeFont BookmarkTableView::GetAltTextFont() {
  return ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::BaseFont);
}