// Copyright (c) 2009 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_bubble_view.h"

#include "app/gfx/canvas.h"
#include "app/gfx/color_utils.h"
#include "app/l10n_util.h"
#include "app/resource_bundle.h"
#include "base/keyboard_codes.h"
#include "base/string_util.h"
#include "chrome/app/chrome_dll_resource.h"
#include "chrome/browser/bookmarks/bookmark_editor.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/bookmarks/bookmark_utils.h"
#include "chrome/browser/metrics/user_metrics.h"
#include "chrome/browser/profile.h"
#include "chrome/browser/views/info_bubble.h"
#include "chrome/common/notification_service.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "views/event.h"
#include "views/standard_layout.h"
#include "views/controls/button/native_button.h"
#include "views/controls/textfield/textfield.h"
#include "views/focus/focus_manager.h"

using views::Combobox;
using views::ColumnSet;
using views::GridLayout;
using views::Label;
using views::Link;
using views::NativeButton;
using views::View;

// Padding between "Title:" and the actual title.
static const int kTitlePadding = 4;

// Minimum width for the fields - they will push out the size of the bubble if
// necessary. This should be big enough so that the field pushes the right side
// of the bubble far enough so that the edit button's left edge is to the right
// of the field's left edge.
static const int kMinimumFieldSize = 180;

// Max number of most recently used folders.
static const size_t kMaxMRUFolders = 5;

// Bubble close image.
static SkBitmap* kCloseImage = NULL;

// Declared in browser_dialogs.h so callers don't have to depend on our header.

namespace browser {

void ShowBookmarkBubbleView(views::Window* parent,
                            const gfx::Rect& bounds,
                            InfoBubbleDelegate* delegate,
                            Profile* profile,
                            const GURL& url,
                            bool newly_bookmarked) {
  BookmarkBubbleView::Show(parent, bounds, delegate, profile, url,
                           newly_bookmarked);
}

void HideBookmarkBubbleView() {
  BookmarkBubbleView::Hide();
}

bool IsBookmarkBubbleViewShowing() {
  return BookmarkBubbleView::IsShowing();
}

}  // namespace browser

// RecentlyUsedFoldersModel ---------------------------------------------------

BookmarkBubbleView::RecentlyUsedFoldersModel::RecentlyUsedFoldersModel(
    BookmarkModel* bb_model, const BookmarkNode* node)
      // Use + 2 to account for bookmark bar and other node.
      : nodes_(bookmark_utils::GetMostRecentlyModifiedGroups(
            bb_model, kMaxMRUFolders + 2)),
      node_parent_index_(0) {
  // TODO(sky): bug 1173415 add a separator in the combobox here.

  // We special case the placement of these, so remove them from the list, then
  // fix up the order.
  RemoveNode(bb_model->GetBookmarkBarNode());
  RemoveNode(bb_model->other_node());
  RemoveNode(node->GetParent());

  // Make the parent the first item, unless it's the bookmark bar or other node.
  if (node->GetParent() != bb_model->GetBookmarkBarNode() &&
      node->GetParent() != bb_model->other_node()) {
    nodes_.insert(nodes_.begin(), node->GetParent());
  }

  // Make sure we only have kMaxMRUFolders in the first chunk.
  if (nodes_.size() > kMaxMRUFolders)
    nodes_.erase(nodes_.begin() + kMaxMRUFolders, nodes_.end());

  // And put the bookmark bar and other nodes at the end of the list.
  nodes_.push_back(bb_model->GetBookmarkBarNode());
  nodes_.push_back(bb_model->other_node());

  node_parent_index_ = static_cast<int>(
      find(nodes_.begin(), nodes_.end(), node->GetParent()) - nodes_.begin());
}

int BookmarkBubbleView::RecentlyUsedFoldersModel::GetItemCount() {
  return static_cast<int>(nodes_.size() + 1);
}

std::wstring BookmarkBubbleView::RecentlyUsedFoldersModel::GetItemAt(
    int index) {
  if (index == static_cast<int>(nodes_.size()))
    return l10n_util::GetString(IDS_BOOMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER);
  return nodes_[index]->GetTitle();
}

const BookmarkNode* BookmarkBubbleView::RecentlyUsedFoldersModel::GetNodeAt(
    int index) {
  return nodes_[index];
}

void BookmarkBubbleView::RecentlyUsedFoldersModel::RemoveNode(
    const BookmarkNode* node) {
  std::vector<const BookmarkNode*>::iterator i =
      find(nodes_.begin(), nodes_.end(), node);
  if (i != nodes_.end())
    nodes_.erase(i);
}

// BookmarkBubbleView ---------------------------------------------------------

BookmarkBubbleView* BookmarkBubbleView::bubble_ = NULL;

// static
void BookmarkBubbleView::Show(views::Window* parent,
                              const gfx::Rect& bounds,
                              InfoBubbleDelegate* delegate,
                              Profile* profile,
                              const GURL& url,
                              bool newly_bookmarked) {
  if (IsShowing())
    return;

  bubble_ = new BookmarkBubbleView(delegate, profile, url, newly_bookmarked);
  InfoBubble::Show(parent, bounds, bubble_, bubble_);
  GURL url_ptr(url);
  NotificationService::current()->Notify(
      NotificationType::BOOKMARK_BUBBLE_SHOWN,
      Source<Profile>(profile->GetOriginalProfile()),
      Details<GURL>(&url_ptr));
  bubble_->BubbleShown();
}

// static
bool BookmarkBubbleView::IsShowing() {
  return bubble_ != NULL;
}

void BookmarkBubbleView::Hide() {
  if (IsShowing())
    bubble_->Close();
}

BookmarkBubbleView::~BookmarkBubbleView() {
  if (apply_edits_) {
    ApplyEdits();
  } else if (remove_bookmark_) {
    BookmarkModel* model = profile_->GetBookmarkModel();
    const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_);
    if (node)
      model->Remove(node->GetParent(), node->GetParent()->IndexOfChild(node));
  }
}

void BookmarkBubbleView::DidChangeBounds(const gfx::Rect& previous,
                                         const gfx::Rect& current) {
  Layout();
}

void BookmarkBubbleView::BubbleShown() {
  DCHECK(GetWidget());
  GetFocusManager()->RegisterAccelerator(
      views::Accelerator(base::VKEY_RETURN, false, false, false), this);

  title_tf_->RequestFocus();
  title_tf_->SelectAll();
}

bool BookmarkBubbleView::AcceleratorPressed(
    const views::Accelerator& accelerator) {
  if (accelerator.GetKeyCode() != base::VKEY_RETURN)
    return false;

  if (edit_button_->HasFocus())
    HandleButtonPressed(edit_button_);
  else
    HandleButtonPressed(close_button_);
  return true;
}

void BookmarkBubbleView::ViewHierarchyChanged(bool is_add, View* parent,
                                              View* child) {
  if (is_add && child == this)
    Init();
}

BookmarkBubbleView::BookmarkBubbleView(InfoBubbleDelegate* delegate,
                                       Profile* profile,
                                       const GURL& url,
                                       bool newly_bookmarked)
    : delegate_(delegate),
      profile_(profile),
      url_(url),
      newly_bookmarked_(newly_bookmarked),
      parent_model_(
          profile_->GetBookmarkModel(),
          profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url)),
      remove_bookmark_(false),
      apply_edits_(true) {
}

void BookmarkBubbleView::Init() {
  static SkColor kTitleColor;
  static bool initialized = false;
  if (!initialized) {
    kTitleColor = color_utils::GetReadableColor(SkColorSetRGB(6, 45, 117),
                                                InfoBubble::kBackgroundColor);
    kCloseImage = ResourceBundle::GetSharedInstance().GetBitmapNamed(
      IDR_INFO_BUBBLE_CLOSE);

    initialized = true;
  }

  remove_link_ = new Link(l10n_util::GetString(
      IDS_BOOMARK_BUBBLE_REMOVE_BOOKMARK));
  remove_link_->SetController(this);

  edit_button_ = new NativeButton(
      this, l10n_util::GetString(IDS_BOOMARK_BUBBLE_OPTIONS));

  close_button_ = new NativeButton(this, l10n_util::GetString(IDS_CLOSE));
  close_button_->SetIsDefault(true);

  parent_combobox_ = new Combobox(&parent_model_);
  parent_combobox_->SetSelectedItem(parent_model_.node_parent_index());
  parent_combobox_->set_listener(this);

  Label* title_label = new Label(l10n_util::GetString(
      newly_bookmarked_ ? IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED :
                          IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK));
  title_label->SetFont(
      ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont));
  title_label->SetColor(kTitleColor);

  GridLayout* layout = new GridLayout(this);
  SetLayoutManager(layout);

  ColumnSet* cs = layout->AddColumnSet(0);

  // Top (title) row.
  cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF,
                0, 0);
  cs->AddPaddingColumn(1, kUnrelatedControlHorizontalSpacing);
  cs->AddColumn(GridLayout::CENTER, GridLayout::CENTER, 0, GridLayout::USE_PREF,
                0, 0);

  // Middle (input field) rows.
  cs = layout->AddColumnSet(2);
  cs->AddColumn(GridLayout::LEADING, GridLayout::CENTER, 0,
                GridLayout::USE_PREF, 0, 0);
  cs->AddPaddingColumn(0, kRelatedControlHorizontalSpacing);
  cs->AddColumn(GridLayout::FILL, GridLayout::CENTER, 1,
                GridLayout::USE_PREF, 0, kMinimumFieldSize);

  // Bottom (buttons) row.
  cs = layout->AddColumnSet(3);
  cs->AddPaddingColumn(1, kRelatedControlHorizontalSpacing);
  cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0,
                GridLayout::USE_PREF, 0, 0);
  // We subtract 2 to account for the natural button padding, and
  // to bring the separation visually in line with the row separation
  // height.
  cs->AddPaddingColumn(0, kRelatedButtonHSpacing - 2);
  cs->AddColumn(GridLayout::LEADING, GridLayout::TRAILING, 0,
                GridLayout::USE_PREF, 0, 0);

  layout->StartRow(0, 0);
  layout->AddView(title_label);
  layout->AddView(remove_link_);

  layout->AddPaddingRow(0, kRelatedControlSmallVerticalSpacing);
  layout->StartRow(0, 2);
  layout->AddView(
      new Label(l10n_util::GetString(IDS_BOOMARK_BUBBLE_TITLE_TEXT)));
  title_tf_ = new views::Textfield();
  title_tf_->SetText(WideToUTF16(GetTitle()));
  layout->AddView(title_tf_);

  layout->AddPaddingRow(0, kRelatedControlSmallVerticalSpacing);

  layout->StartRow(0, 2);
  layout->AddView(
      new Label(l10n_util::GetString(IDS_BOOMARK_BUBBLE_FOLDER_TEXT)));
  layout->AddView(parent_combobox_);
  layout->AddPaddingRow(0, kRelatedControlSmallVerticalSpacing);

  layout->StartRow(0, 3);
  layout->AddView(edit_button_);
  layout->AddView(close_button_);
}

std::wstring BookmarkBubbleView::GetTitle() {
  BookmarkModel* bookmark_model= profile_->GetBookmarkModel();
  const BookmarkNode* node =
      bookmark_model->GetMostRecentlyAddedNodeForURL(url_);
  if (node)
    return node->GetTitle();
  else
    NOTREACHED();
  return std::wstring();
}

void BookmarkBubbleView::ButtonPressed(
    views::Button* sender, const views::Event& event) {
  HandleButtonPressed(sender);
}

void BookmarkBubbleView::LinkActivated(Link* source, int event_flags) {
  DCHECK(source == remove_link_);
  UserMetrics::RecordAction(L"BookmarkBubble_Unstar", profile_);

  // Set this so we remove the bookmark after the window closes.
  remove_bookmark_ = true;
  apply_edits_ = false;

  Close();
}

void BookmarkBubbleView::ItemChanged(Combobox* combobox,
                                     int prev_index,
                                     int new_index) {
  if (new_index + 1 == parent_model_.GetItemCount()) {
    UserMetrics::RecordAction(L"BookmarkBubble_EditFromCombobox", profile_);

    ShowEditor();
    return;
  }
}

void BookmarkBubbleView::InfoBubbleClosing(InfoBubble* info_bubble,
                                           bool closed_by_escape) {
  if (closed_by_escape) {
    remove_bookmark_ = newly_bookmarked_;
    apply_edits_ = false;
  }

  // We have to reset |bubble_| here, not in our destructor, because we'll be
  // destroyed asynchronously and the shown state will be checked before then.
  DCHECK(bubble_ == this);
  bubble_ = NULL;

  if (delegate_)
    delegate_->InfoBubbleClosing(info_bubble, closed_by_escape);
  NotificationService::current()->Notify(
      NotificationType::BOOKMARK_BUBBLE_HIDDEN,
      Source<Profile>(profile_->GetOriginalProfile()),
      NotificationService::NoDetails());
}

bool BookmarkBubbleView::CloseOnEscape() {
  return delegate_ ? delegate_->CloseOnEscape() : true;
}

void BookmarkBubbleView::Close() {
  static_cast<InfoBubble*>(GetWidget())->Close();
}

void BookmarkBubbleView::HandleButtonPressed(views::Button* sender) {
  if (sender == edit_button_) {
    UserMetrics::RecordAction(L"BookmarkBubble_Edit", profile_);
    ShowEditor();
  } else {
    DCHECK(sender == close_button_);
    Close();
  }
  // WARNING: we've most likely been deleted when CloseWindow returns.
}

void BookmarkBubbleView::ShowEditor() {
  const BookmarkNode* node =
      profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url_);

  // Commit any edits now.
  ApplyEdits();

#if defined(OS_WIN)
  // Parent the editor to our root ancestor (not the root we're in, as that
  // is the info bubble and will close shortly).
  HWND parent = GetAncestor(GetWidget()->GetNativeView(), GA_ROOTOWNER);

  // We're about to show the bookmark editor. When the bookmark editor closes
  // we want the browser to become active. WidgetWin::Hide() does a hide in
  // a such way that activation isn't changed, which means when we close
  // Windows gets confused as to who it should give active status to. We
  // explicitly hide the bookmark bubble window in such a way that activation
  // status changes. That way, when the editor closes, activation is properly
  // restored to the browser.
  ShowWindow(GetWidget()->GetNativeView(), SW_HIDE);
#else
  gfx::NativeWindow parent = GTK_WINDOW(
      static_cast<views::WidgetGtk*>(GetWidget())->GetTransientParent());
#endif

  // Even though we just hid the window, we need to invoke Close to schedule
  // the delete and all that.
  Close();

  if (node) {
    BookmarkEditor::Show(parent, profile_, NULL,
                         BookmarkEditor::EditDetails(node),
                         BookmarkEditor::SHOW_TREE, NULL);
  }
}

void BookmarkBubbleView::ApplyEdits() {
  // Set this to make sure we don't attempt to apply edits again.
  apply_edits_ = false;

  BookmarkModel* model = profile_->GetBookmarkModel();
  const BookmarkNode* node = model->GetMostRecentlyAddedNodeForURL(url_);
  if (node) {
    const std::wstring new_title = UTF16ToWide(title_tf_->text());
    if (new_title != node->GetTitle()) {
      model->SetTitle(node, new_title);
      UserMetrics::RecordAction(L"BookmarkBubble_ChangeTitleInBubble",
                                profile_);
    }
    // Last index means 'Choose another folder...'
    if (parent_combobox_->selected_item() <
        parent_model_.GetItemCount() - 1) {
      const BookmarkNode* new_parent =
          parent_model_.GetNodeAt(parent_combobox_->selected_item());
      if (new_parent != node->GetParent()) {
        UserMetrics::RecordAction(L"BookmarkBubble_ChangeParent", profile_);
        model->Move(node, new_parent, new_parent->GetChildCount());
      }
    }
  }
}