// 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/ui/views/bookmarks/bookmark_bubble_view.h"

#include "base/string16.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.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/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/views/bubble/bubble.h"
#include "content/common/notification_service.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "ui/base/keycodes/keyboard_codes.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "views/controls/button/native_button.h"
#include "views/controls/textfield/textfield.h"
#include "views/events/event.h"
#include "views/focus/focus_manager.h"
#include "views/layout/grid_layout.h"
#include "views/layout/layout_constants.h"
#include "views/window/client_view.h"
#include "views/window/window.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;

// 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,
                            BubbleDelegate* 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

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

BookmarkBubbleView* BookmarkBubbleView::bookmark_bubble_ = NULL;

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

  bookmark_bubble_ = new BookmarkBubbleView(delegate, profile, url,
                                            newly_bookmarked);
  // TODO(beng): Pass |parent| after V2 is complete.
  Bubble* bubble = Bubble::Show(
      parent->client_view()->GetWidget(), bounds, BubbleBorder::TOP_RIGHT,
      bookmark_bubble_, bookmark_bubble_);
  // |bubble_| can be set to NULL in BubbleClosing when we close the bubble
  // asynchronously. However, that can happen during the Show call above if the
  // window loses activation while we are getting to ready to show the bubble,
  // so we must check to make sure we still have a valid bubble before
  // proceeding.
  if (!bookmark_bubble_)
    return;
  bookmark_bubble_->set_bubble(bubble);
  bubble->SizeToContents();
  GURL url_ptr(url);
  NotificationService::current()->Notify(
      NotificationType::BOOKMARK_BUBBLE_SHOWN,
      Source<Profile>(profile->GetOriginalProfile()),
      Details<GURL>(&url_ptr));
  bookmark_bubble_->BubbleShown();
}

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

void BookmarkBubbleView::Hide() {
  if (IsShowing())
    bookmark_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->parent(), node->parent()->GetIndexOf(node));
  }
}

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

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

bool BookmarkBubbleView::AcceleratorPressed(
    const views::Accelerator& accelerator) {
  if (accelerator.GetKeyCode() != ui::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(BubbleDelegate* 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),
                                                Bubble::kBackgroundColor);
    kCloseImage = ResourceBundle::GetSharedInstance().GetBitmapNamed(
      IDR_INFO_BUBBLE_CLOSE);

    initialized = true;
  }

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

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

  close_button_ =
      new NativeButton(this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_DONE)));
  close_button_->SetIsDefault(true);

  Label* combobox_label = new Label(
      UTF16ToWide(l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_FOLDER_TEXT)));

  parent_combobox_ = new Combobox(&parent_model_);
  parent_combobox_->SetSelectedItem(parent_model_.node_parent_index());
  parent_combobox_->set_listener(this);
  parent_combobox_->SetAccessibleName(
      WideToUTF16Hack(combobox_label->GetText()));
#if defined(TOUCH_UI)
  // TODO(saintlou): This is a short term workaround for touch
  parent_combobox_->SetEnabled(false);
#endif

  Label* title_label = new Label(UTF16ToWide(l10n_util::GetStringUTF16(
      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, views::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, views::kRelatedControlHorizontalSpacing);
  cs->AddColumn(GridLayout::FILL, GridLayout::CENTER, 1,
                GridLayout::USE_PREF, 0, kMinimumFieldSize);

  // Bottom (buttons) row.
  cs = layout->AddColumnSet(3);
  cs->AddPaddingColumn(1, views::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, views::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, views::kRelatedControlSmallVerticalSpacing);
  layout->StartRow(0, 2);
  layout->AddView(new Label(UTF16ToWide(
      l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_TITLE_TEXT))));
  title_tf_ = new views::Textfield();
  title_tf_->SetText(GetTitle());
  layout->AddView(title_tf_);

  layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);

  layout->StartRow(0, 2);
  layout->AddView(combobox_label);
  layout->AddView(parent_combobox_);
  layout->AddPaddingRow(0, views::kRelatedControlSmallVerticalSpacing);

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

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

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(UserMetricsAction("BookmarkBubble_Unstar"),
                            profile_);

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

  bubble_->set_fade_away_on_close(true);
  Close();
}

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

    ShowEditor();
    return;
  }
}

void BookmarkBubbleView::BubbleClosing(Bubble* 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(bookmark_bubble_ == this);
  bookmark_bubble_ = NULL;

  if (delegate_)
    delegate_->BubbleClosing(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;
}

bool BookmarkBubbleView::FadeInOnShow() {
  return false;
}

std::wstring BookmarkBubbleView::accessible_name() {
  return UTF16ToWide(
      l10n_util::GetStringUTF16(IDS_BOOMARK_BUBBLE_ADD_BOOKMARK));
}

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

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

void BookmarkBubbleView::ShowEditor() {
#if defined(TOUCH_UI)
  // Close the Bubble
  Close();

  // Open the Bookmark Manager
  Browser* browser = BrowserList::GetLastActiveWithProfile(profile_);
  DCHECK(browser);
  if (browser)
    browser->OpenBookmarkManager();
  else
    NOTREACHED();

#else
  const BookmarkNode* node =
      profile_->GetBookmarkModel()->GetMostRecentlyAddedNodeForURL(url_);

#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);
  }
#endif
}

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 string16 new_title = title_tf_->text();
    if (new_title != node->GetTitle()) {
      model->SetTitle(node, new_title);
      UserMetrics::RecordAction(
          UserMetricsAction("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->parent()) {
        UserMetrics::RecordAction(
            UserMetricsAction("BookmarkBubble_ChangeParent"), profile_);
        model->Move(node, new_parent, new_parent->child_count());
      }
    }
  }
}