// 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/speech/speech_input_bubble.h"

#include <algorithm>

#include "base/message_loop.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/views/bubble/bubble.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/browser/tab_contents/tab_contents_view.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "media/audio/audio_manager.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/canvas.h"
#include "views/border.h"
#include "views/controls/button/native_button.h"
#include "views/controls/image_view.h"
#include "views/controls/label.h"
#include "views/controls/link.h"
#include "views/layout/layout_constants.h"
#include "views/view.h"

namespace {

const int kBubbleHorizMargin = 6;
const int kBubbleVertMargin = 4;
const int kBubbleHeadingVertMargin = 6;

// This is the content view which is placed inside a SpeechInputBubble.
class ContentView
    : public views::View,
      public views::ButtonListener,
      public views::LinkController {
 public:
  explicit ContentView(SpeechInputBubbleDelegate* delegate);

  void UpdateLayout(SpeechInputBubbleBase::DisplayMode mode,
                    const string16& message_text,
                    const SkBitmap& image);
  void SetImage(const SkBitmap& image);

  // views::ButtonListener methods.
  virtual void ButtonPressed(views::Button* source, const views::Event& event);

  // views::LinkController methods.
  virtual void LinkActivated(views::Link* source, int event_flags);

  // views::View overrides.
  virtual gfx::Size GetPreferredSize();
  virtual void Layout();

 private:
  SpeechInputBubbleDelegate* delegate_;
  views::ImageView* icon_;
  views::Label* heading_;
  views::Label* message_;
  views::NativeButton* try_again_;
  views::NativeButton* cancel_;
  views::Link* mic_settings_;
  SpeechInputBubbleBase::DisplayMode display_mode_;
  const int kIconLayoutMinWidth;

  DISALLOW_COPY_AND_ASSIGN(ContentView);
};

ContentView::ContentView(SpeechInputBubbleDelegate* delegate)
     : delegate_(delegate),
       display_mode_(SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP),
       kIconLayoutMinWidth(ResourceBundle::GetSharedInstance().GetBitmapNamed(
                           IDR_SPEECH_INPUT_MIC_EMPTY)->width()) {
  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
  const gfx::Font& font = rb.GetFont(ResourceBundle::MediumFont);

  heading_ = new views::Label(
      UTF16ToWide(l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_BUBBLE_HEADING)));
  heading_->set_border(views::Border::CreateEmptyBorder(
      kBubbleHeadingVertMargin, 0, kBubbleHeadingVertMargin, 0));
  heading_->SetFont(font);
  heading_->SetHorizontalAlignment(views::Label::ALIGN_CENTER);
  heading_->SetText(UTF16ToWide(
      l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_BUBBLE_HEADING)));
  AddChildView(heading_);

  message_ = new views::Label();
  message_->SetFont(font);
  message_->SetHorizontalAlignment(views::Label::ALIGN_CENTER);
  message_->SetMultiLine(true);
  AddChildView(message_);

  icon_ = new views::ImageView();
  icon_->SetHorizontalAlignment(views::ImageView::CENTER);
  AddChildView(icon_);

  cancel_ = new views::NativeButton(
      this,
      UTF16ToWide(l10n_util::GetStringUTF16(IDS_CANCEL)));
  AddChildView(cancel_);

  try_again_ = new views::NativeButton(
      this,
      UTF16ToWide(l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_TRY_AGAIN)));
  AddChildView(try_again_);

  mic_settings_ = new views::Link(
      UTF16ToWide(l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_MIC_SETTINGS)));
  mic_settings_->SetController(this);
  AddChildView(mic_settings_);
}

void ContentView::UpdateLayout(SpeechInputBubbleBase::DisplayMode mode,
                               const string16& message_text,
                               const SkBitmap& image) {
  display_mode_ = mode;
  bool is_message = (mode == SpeechInputBubbleBase::DISPLAY_MODE_MESSAGE);
  icon_->SetVisible(!is_message);
  message_->SetVisible(is_message);
  mic_settings_->SetVisible(is_message);
  try_again_->SetVisible(is_message);
  cancel_->SetVisible(mode != SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP);
  heading_->SetVisible(mode == SpeechInputBubbleBase::DISPLAY_MODE_RECORDING);

  if (is_message) {
    message_->SetText(UTF16ToWideHack(message_text));
  } else {
    SetImage(image);
  }

  if (icon_->IsVisible())
    icon_->ResetImageSize();

  // When moving from warming up to recording state, the size of the content
  // stays the same. So we wouldn't get a resize/layout call from the view
  // system and we do it ourselves.
  if (GetPreferredSize() == size())  // |size()| here is the current size.
    Layout();
}

void ContentView::SetImage(const SkBitmap& image) {
  icon_->SetImage(image);
}

void ContentView::ButtonPressed(views::Button* source,
                                const views::Event& event) {
  if (source == cancel_) {
    delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_CANCEL);
  } else if (source == try_again_) {
    delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_TRY_AGAIN);
  } else {
    NOTREACHED() << "Unknown button";
  }
}

void ContentView::LinkActivated(views::Link* source, int event_flags) {
  DCHECK_EQ(source, mic_settings_);
  AudioManager::GetAudioManager()->ShowAudioInputSettings();
}

gfx::Size ContentView::GetPreferredSize() {
  int width = heading_->GetPreferredSize().width();
  int control_width = cancel_->GetPreferredSize().width();
  if (try_again_->IsVisible()) {
    control_width += try_again_->GetPreferredSize().width() +
                     views::kRelatedButtonHSpacing;
  }
  width = std::max(width, control_width);
  control_width = std::max(icon_->GetPreferredSize().width(),
                           kIconLayoutMinWidth);
  width = std::max(width, control_width);
  if (mic_settings_->IsVisible()) {
    control_width = mic_settings_->GetPreferredSize().width();
    width = std::max(width, control_width);
  }

  int height = cancel_->GetPreferredSize().height();
  if (message_->IsVisible()) {
    height += message_->GetHeightForWidth(width) +
              views::kLabelToControlVerticalSpacing;
  }
  if (heading_->IsVisible())
    height += heading_->GetPreferredSize().height();
  if (icon_->IsVisible())
    height += icon_->GetImage().height();
  if (mic_settings_->IsVisible())
    height += mic_settings_->GetPreferredSize().height();
  width += kBubbleHorizMargin * 2;
  height += kBubbleVertMargin * 2;

  return gfx::Size(width, height);
}

void ContentView::Layout() {
  int x = kBubbleHorizMargin;
  int y = kBubbleVertMargin;
  int available_width = width() - kBubbleHorizMargin * 2;
  int available_height = height() - kBubbleVertMargin * 2;

  if (message_->IsVisible()) {
    DCHECK(try_again_->IsVisible());

    int control_height = try_again_->GetPreferredSize().height();
    int try_again_width = try_again_->GetPreferredSize().width();
    int cancel_width = cancel_->GetPreferredSize().width();
    y += available_height - control_height;
    x += (available_width - cancel_width - try_again_width -
          views::kRelatedButtonHSpacing) / 2;
    try_again_->SetBounds(x, y, try_again_width, control_height);
    cancel_->SetBounds(x + try_again_width + views::kRelatedButtonHSpacing, y,
                       cancel_width, control_height);

    control_height = message_->GetHeightForWidth(available_width);
    message_->SetBounds(kBubbleHorizMargin, kBubbleVertMargin,
                        available_width, control_height);
    y = kBubbleVertMargin + control_height;

    control_height = mic_settings_->GetPreferredSize().height();
    mic_settings_->SetBounds(kBubbleHorizMargin, y, available_width,
                             control_height);
  } else {
    DCHECK(icon_->IsVisible());

    int control_height = icon_->GetImage().height();
    if (display_mode_ == SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP)
      y = (available_height - control_height) / 2;
    icon_->SetBounds(x, y, available_width, control_height);
    y += control_height;

    if (heading_->IsVisible()) {
      control_height = heading_->GetPreferredSize().height();
      heading_->SetBounds(x, y, available_width, control_height);
      y += control_height;
    }

    if (cancel_->IsVisible()) {
      control_height = cancel_->GetPreferredSize().height();
      int width = cancel_->GetPreferredSize().width();
      cancel_->SetBounds(x + (available_width - width) / 2, y, width,
                         control_height);
    }
  }
}

// Implementation of SpeechInputBubble.
class SpeechInputBubbleImpl
    : public SpeechInputBubbleBase,
      public BubbleDelegate {
 public:
  SpeechInputBubbleImpl(TabContents* tab_contents,
                        Delegate* delegate,
                        const gfx::Rect& element_rect);
  virtual ~SpeechInputBubbleImpl();

  // SpeechInputBubble methods.
  virtual void Show();
  virtual void Hide();

  // SpeechInputBubbleBase methods.
  virtual void UpdateLayout();
  virtual void UpdateImage();

  // Returns the screen rectangle to use as the info bubble's target.
  // |element_rect| is the html element's bounds in page coordinates.
  gfx::Rect GetInfoBubbleTarget(const gfx::Rect& element_rect);

  // BubbleDelegate
  virtual void BubbleClosing(Bubble* bubble, bool closed_by_escape);
  virtual bool CloseOnEscape();
  virtual bool FadeInOnShow();

 private:
  Delegate* delegate_;
  Bubble* bubble_;
  ContentView* bubble_content_;
  gfx::Rect element_rect_;

  // Set to true if the object is being destroyed normally instead of the
  // user clicking outside the window causing it to close automatically.
  bool did_invoke_close_;

  DISALLOW_COPY_AND_ASSIGN(SpeechInputBubbleImpl);
};

SpeechInputBubbleImpl::SpeechInputBubbleImpl(TabContents* tab_contents,
                                             Delegate* delegate,
                                             const gfx::Rect& element_rect)
    : SpeechInputBubbleBase(tab_contents),
      delegate_(delegate),
      bubble_(NULL),
      bubble_content_(NULL),
      element_rect_(element_rect),
      did_invoke_close_(false) {
}

SpeechInputBubbleImpl::~SpeechInputBubbleImpl() {
  did_invoke_close_ = true;
  Hide();
}

gfx::Rect SpeechInputBubbleImpl::GetInfoBubbleTarget(
    const gfx::Rect& element_rect) {
  gfx::Rect container_rect;
  tab_contents()->GetContainerBounds(&container_rect);
  return gfx::Rect(
      container_rect.x() + element_rect.x() + element_rect.width() -
          kBubbleTargetOffsetX,
      container_rect.y() + element_rect.y() + element_rect.height(), 1, 1);
}

void SpeechInputBubbleImpl::BubbleClosing(Bubble* bubble,
                                          bool closed_by_escape) {
  bubble_ = NULL;
  bubble_content_ = NULL;
  if (!did_invoke_close_)
    delegate_->InfoBubbleFocusChanged();
}

bool SpeechInputBubbleImpl::CloseOnEscape() {
  return false;
}

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

void SpeechInputBubbleImpl::Show() {
  if (bubble_)
    return;  // nothing to do, already visible.

  bubble_content_ = new ContentView(delegate_);
  UpdateLayout();

  views::NativeWidget* toplevel_widget =
      views::NativeWidget::GetTopLevelNativeWidget(
          tab_contents()->view()->GetNativeView());
  if (toplevel_widget) {
    bubble_ = Bubble::Show(toplevel_widget->GetWidget(),
                           GetInfoBubbleTarget(element_rect_),
                           BubbleBorder::TOP_LEFT, bubble_content_,
                           this);

    // We don't want fade outs when closing because it makes speech recognition
    // appear slower than it is. Also setting it to false allows |Close| to
    // destroy the bubble immediately instead of waiting for the fade animation
    // to end so the caller can manage this object's life cycle like a normal
    // stack based or member variable object.
    bubble_->set_fade_away_on_close(false);
  }
}

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

void SpeechInputBubbleImpl::UpdateLayout() {
  if (bubble_content_)
    bubble_content_->UpdateLayout(display_mode(), message_text(), icon_image());
  if (bubble_)  // Will be null on first call.
    bubble_->SizeToContents();
}

void SpeechInputBubbleImpl::UpdateImage() {
  if (bubble_content_)
    bubble_content_->SetImage(icon_image());
}

}  // namespace

SpeechInputBubble* SpeechInputBubble::CreateNativeBubble(
    TabContents* tab_contents,
    SpeechInputBubble::Delegate* delegate,
    const gfx::Rect& element_rect) {
  return new SpeechInputBubbleImpl(tab_contents, delegate, element_rect);
}