// Copyright (c) 2012 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/gtk/chrome_to_mobile_bubble_gtk.h"

#include <gtk/gtk.h>

#include <string>

#include "base/basictypes.h"
#include "base/bind.h"
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "base/message_loop.h"
#include "base/string16.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/chrome_to_mobile_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/gtk/gtk_util.h"
#include "chrome/browser/ui/gtk/theme_service_gtk.h"
#include "chrome/common/chrome_notification_types.h"
#include "content/public/browser/notification_source.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources_standard.h"
#include "ui/base/animation/throb_animation.h"
#include "ui/base/gtk/gtk_hig_constants.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/text/bytes_formatting.h"
#include "ui/gfx/image/image.h"

namespace {

// The currently open app-modal bubble singleton, or NULL if none is open.
ChromeToMobileBubbleGtk* g_bubble = NULL;

// Padding between the content and the edge of bubble; from BookmarkBubbleGtk.
const int kContentBorder = 7;

// Horizontal padding that preceeds mobile device radio buttons.
const int kRadioPadding = 20;

// The millisecond duration of the "Sending..." progress throb animation.
const size_t kProgressThrobDurationMS = 2400;

// The seconds to delay before automatically closing the bubble after sending.
const int kAutoCloseDelay = 3;

// The color of the error label.
const GdkColor kErrorColor = GDK_COLOR_RGB(0xFF, 0x00, 0x00);

}  // namespace

// static
void ChromeToMobileBubbleGtk::Show(GtkImage* anchor_image, Profile* profile) {
  // Do not construct a new bubble if one is already being shown.
  if (!g_bubble)
    g_bubble = new ChromeToMobileBubbleGtk(anchor_image, profile);
}

void ChromeToMobileBubbleGtk::BubbleClosing(BubbleGtk* bubble,
                                            bool closed_by_escape) {
  DCHECK_EQ(bubble, bubble_);

  gtk_image_set_from_pixbuf(GTK_IMAGE(anchor_image_),
      theme_service_->GetImageNamed(IDR_MOBILE)->ToGdkPixbuf());

  labels_.clear();
  anchor_image_ = NULL;
  send_copy_ = NULL;
  cancel_ = NULL;
  send_ = NULL;
  error_ = NULL;
  bubble_ = NULL;
  progress_animation_.reset();
}

void ChromeToMobileBubbleGtk::AnimationProgressed(
    const ui::Animation* animation) {
  DCHECK(animation == progress_animation_.get());
  double animation_value = animation->GetCurrentValue();
  int id = IDS_CHROME_TO_MOBILE_BUBBLE_SENDING_3;
  // Show each of four messages for 1/4 of the animation.
  if (animation_value < 0.25)
    id = IDS_CHROME_TO_MOBILE_BUBBLE_SENDING_0;
  else if (animation_value < 0.5)
    id = IDS_CHROME_TO_MOBILE_BUBBLE_SENDING_1;
  else if (animation_value < 0.75)
    id = IDS_CHROME_TO_MOBILE_BUBBLE_SENDING_2;
  gtk_button_set_label(GTK_BUTTON(send_), l10n_util::GetStringUTF8(id).c_str());
}

void ChromeToMobileBubbleGtk::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  DCHECK(type == chrome::NOTIFICATION_BROWSER_THEME_CHANGED);

  // Update the tracked labels to match the new theme, as in BookmarkBubbleGtk.
  const GdkColor* color =
      theme_service_->UsingNativeTheme() ? NULL : &ui::kGdkBlack;
  std::vector<GtkWidget*>::iterator it;
  for (it = labels_.begin(); it != labels_.end(); ++it)
    gtk_widget_modify_fg(*it, GTK_STATE_NORMAL, color);
}

void ChromeToMobileBubbleGtk::SnapshotGenerated(const FilePath& path,
                                                int64 bytes) {
  if (bytes > 0) {
    snapshot_path_ = path;
    gtk_button_set_label(GTK_BUTTON(send_copy_), l10n_util::GetStringFUTF8(
        IDS_CHROME_TO_MOBILE_BUBBLE_SEND_COPY, ui::FormatBytes(bytes)).c_str());
    gtk_widget_set_sensitive(send_copy_, TRUE);
  } else {
    gtk_button_set_label(GTK_BUTTON(send_copy_), l10n_util::GetStringUTF8(
        IDS_CHROME_TO_MOBILE_BUBBLE_SEND_COPY_FAILED).c_str());
  }
}

void ChromeToMobileBubbleGtk::OnSendComplete(bool success) {
  progress_animation_->Stop();
  gtk_button_set_alignment(GTK_BUTTON(send_), 0.5, 0.5);

  if (success) {
    gtk_button_set_label(GTK_BUTTON(send_),
        l10n_util::GetStringUTF8(IDS_CHROME_TO_MOBILE_BUBBLE_SENT).c_str());
    MessageLoop::current()->PostDelayedTask(FROM_HERE,
        base::Bind(&ChromeToMobileBubbleGtk::OnCancelClicked,
                   weak_ptr_factory_.GetWeakPtr(), GTK_WIDGET(NULL)),
        base::TimeDelta::FromSeconds(kAutoCloseDelay));
  } else {
    gtk_button_set_label(GTK_BUTTON(send_),
        l10n_util::GetStringUTF8(IDS_CHROME_TO_MOBILE_BUBBLE_ERROR).c_str());
    gtk_widget_set_visible(error_, TRUE);
  }
}

ChromeToMobileBubbleGtk::ChromeToMobileBubbleGtk(GtkImage* anchor_image,
                                                 Profile* profile)
    : ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)),
      profile_(profile),
      theme_service_(ThemeServiceGtk::GetFrom(profile_)),
      selected_mobile_(NULL),
      anchor_image_(anchor_image),
      send_copy_(NULL),
      cancel_(NULL),
      send_(NULL),
      error_(NULL),
      bubble_(NULL) {
  ChromeToMobileService* service =
      ChromeToMobileServiceFactory::GetForProfile(profile);

  // Generate the MHTML snapshot now to report its size in the bubble.
  service->GenerateSnapshot(weak_ptr_factory_.GetWeakPtr());

  // Get the list of mobile devices.
  std::vector<DictionaryValue*> mobiles = service->mobiles();
  DCHECK_GT(mobiles.size(), 0U);
  selected_mobile_ = mobiles[0];

  GtkWidget* content = gtk_vbox_new(FALSE, 5);
  gtk_container_set_border_width(GTK_CONTAINER(content), kContentBorder);

  // Create and pack the title label; init the selected mobile device.
  GtkWidget* title = NULL;
  if (mobiles.size() == 1) {
    string16 mobile_name;
    mobiles[0]->GetString("name", &mobile_name);
    title = gtk_label_new(l10n_util::GetStringFUTF8(
        IDS_CHROME_TO_MOBILE_BUBBLE_SINGLE_TITLE, mobile_name).c_str());
  } else {
    title = gtk_label_new(l10n_util::GetStringUTF8(
        IDS_CHROME_TO_MOBILE_BUBBLE_MULTI_TITLE).c_str());
  }
  gtk_misc_set_alignment(GTK_MISC(title), 0, 1);
  gtk_box_pack_start(GTK_BOX(content), title, FALSE, FALSE, 0);
  labels_.push_back(title);

  // Create and pack the device radio group; init the selected mobile device.
  if (mobiles.size() > 1) {
    GtkWidget* radio = NULL;
    for (std::vector<DictionaryValue*>::const_iterator it = mobiles.begin();
         it != mobiles.end(); ++it) {
      std::string name;
      (*it)->GetStringASCII("name", &name);
      radio = gtk_radio_button_new_with_label_from_widget(
                  GTK_RADIO_BUTTON(radio), name.c_str());

      // Activate the default radio button before attaching its signal handler.
      if (mobile_map_.empty()) {
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(radio), TRUE);
        DCHECK_EQ(selected_mobile_, *it);
      }

      mobile_map_[radio] = *it;
      // Pack each radio item into a horizontal box padding.
      GtkWidget* row = gtk_hbox_new(FALSE, 0);
      gtk_box_pack_start(GTK_BOX(row), radio, FALSE, FALSE, kRadioPadding);
      gtk_box_pack_start(GTK_BOX(content), row, FALSE, FALSE, 0);
      g_signal_connect(radio, "toggled", G_CALLBACK(OnRadioToggledThunk), this);
    }
  }

  // Create and pack the offline copy check box.
  send_copy_ = gtk_check_button_new_with_label(
      l10n_util::GetStringFUTF8(IDS_CHROME_TO_MOBILE_BUBBLE_SEND_COPY,
          l10n_util::GetStringUTF16(
              IDS_CHROME_TO_MOBILE_BUBBLE_SEND_COPY_GENERATING)).c_str());
  gtk_widget_set_sensitive(send_copy_, FALSE);
  gtk_box_pack_start(GTK_BOX(content), send_copy_, FALSE, FALSE, 0);

  // Set the send button requested size from its final (presumed longest) string
  // to avoid resizes during animation. Use the same size for the cancel button.
  send_ = gtk_button_new_with_label(
      l10n_util::GetStringUTF8(IDS_CHROME_TO_MOBILE_BUBBLE_SENDING_3).c_str());
  GtkRequisition button_size;
  gtk_widget_size_request(send_, &button_size);
  button_size.width *= 1.25;
  gtk_widget_set_size_request(send_, button_size.width, button_size.height);
  gtk_button_set_label(GTK_BUTTON(send_),
      l10n_util::GetStringUTF8(IDS_CHROME_TO_MOBILE_BUBBLE_SEND).c_str());
  cancel_ = gtk_button_new_with_label(
      l10n_util::GetStringUTF8(IDS_CANCEL).c_str());
  gtk_widget_set_size_request(cancel_, button_size.width, button_size.height);

  // Pack the buttons with an expanding label for right-justification.
  GtkWidget* buttons = gtk_hbox_new(FALSE, 0);
  gtk_box_pack_start(GTK_BOX(buttons), gtk_label_new(""), TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(buttons), cancel_, FALSE, FALSE, 4);
  gtk_box_pack_start(GTK_BOX(buttons), send_, FALSE, FALSE, 0);
  gtk_box_pack_start(GTK_BOX(content), buttons, FALSE, FALSE, 0);

  // Pack the error label, but ensure it doesn't show initially with the bubble.
  error_ = gtk_label_new(l10n_util::GetStringUTF8(
      IDS_CHROME_TO_MOBILE_BUBBLE_ERROR_MESSAGE).c_str());
  gtk_misc_set_alignment(GTK_MISC(error_), 0, 1);
  gtk_util::SetLabelColor(error_, &kErrorColor);
  gtk_box_pack_start(GTK_BOX(content), error_, FALSE, FALSE, 0);
  gtk_widget_set_no_show_all(error_, TRUE);

  // Initialize focus to the send button.
  gtk_container_set_focus_child(GTK_CONTAINER(content), send_);

  BubbleGtk::ArrowLocationGtk arrow_location = base::i18n::IsRTL() ?
      BubbleGtk::ARROW_LOCATION_TOP_LEFT : BubbleGtk::ARROW_LOCATION_TOP_RIGHT;
  bubble_ = BubbleGtk::Show(GTK_WIDGET(anchor_image_), NULL, content,
                arrow_location, true /*match_system_theme*/,
                true /*grab_input*/, theme_service_, this /*delegate*/);
  if (!bubble_) {
    NOTREACHED();
    return;
  }

  g_signal_connect(content, "destroy", G_CALLBACK(&OnDestroyThunk), this);
  g_signal_connect(cancel_, "clicked", G_CALLBACK(&OnCancelClickedThunk), this);
  g_signal_connect(send_, "clicked", G_CALLBACK(&OnSendClickedThunk), this);

  registrar_.Add(this, chrome::NOTIFICATION_BROWSER_THEME_CHANGED,
                 content::Source<ThemeService>(theme_service_));
  theme_service_->InitThemesFor(this);

  gtk_image_set_from_pixbuf(GTK_IMAGE(anchor_image_),
      theme_service_->GetImageNamed(IDR_MOBILE_LIT)->ToGdkPixbuf());
}

ChromeToMobileBubbleGtk::~ChromeToMobileBubbleGtk() {
  DCHECK(g_bubble);
  g_bubble = NULL;
}

void ChromeToMobileBubbleGtk::OnDestroy(GtkWidget* widget) {
  // We are self deleting, we have a destroy signal setup to catch when we
  // destroyed (via the BubbleGtk being destroyed), and delete ourself.
  delete this;
}

void ChromeToMobileBubbleGtk::OnRadioToggled(GtkWidget* widget) {
  DCHECK(mobile_map_.find(widget) != mobile_map_.end());
  selected_mobile_ = mobile_map_.find(widget)->second;
}

void ChromeToMobileBubbleGtk::OnCancelClicked(GtkWidget* widget) {
  bubble_->Close();
}

void ChromeToMobileBubbleGtk::OnSendClicked(GtkWidget* widget) {
  string16 mobile_id;
  selected_mobile_->GetString("id", &mobile_id);
  bool send_copy = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(send_copy_));
  ChromeToMobileServiceFactory::GetForProfile(profile_)->SendToMobile(
      mobile_id, send_copy ? snapshot_path_ : FilePath(),
      weak_ptr_factory_.GetWeakPtr());

  // Update the view's contents to show the "Sending..." progress animation.
  gtk_widget_set_sensitive(cancel_, FALSE);
  gtk_widget_set_sensitive(send_, FALSE);
  gtk_button_set_alignment(GTK_BUTTON(send_), 0, 0.5);
  progress_animation_.reset(new ui::ThrobAnimation(this));
  progress_animation_->SetDuration(kProgressThrobDurationMS);
  progress_animation_->StartThrobbing(-1);
}