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

#include <gtk/gtk.h>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/history/top_sites.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/tab_restore_service.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/themes/theme_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_tab_restore_service_delegate.h"
#include "chrome/browser/ui/gtk/event_utils.h"
#include "chrome/browser/ui/gtk/global_menu_bar.h"
#include "chrome/browser/ui/gtk/gtk_theme_service.h"
#include "chrome/browser/ui/gtk/gtk_util.h"
#include "chrome/common/url_constants.h"
#include "content/public/browser/notification_source.h"
#include "grit/generated_resources.h"
#include "ui/base/gtk/owned_widget_gtk.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/gtk_util.h"
#include "ui/gfx/text_elider.h"

using content::OpenURLParams;

namespace {

// The maximum number of most visited items to display.
const unsigned int kMostVisitedCount = 8;

// The number of recently closed items to get.
const unsigned int kRecentlyClosedCount = 8;

// Menus more than this many chars long will get trimmed.
const int kMaximumMenuWidthInChars = 50;

}  // namespace

struct GlobalHistoryMenu::ClearMenuClosure {
  GtkWidget* container;
  GlobalHistoryMenu* menu_bar;
  int tag;
};

struct GlobalHistoryMenu::GetIndexClosure {
  bool found;
  int current_index;
  int tag;
};

class GlobalHistoryMenu::HistoryItem {
 public:
  HistoryItem()
      : menu_item(NULL),
        session_id(0) {}

  // The title for the menu item.
  string16 title;
  // The URL that will be navigated to if the user selects this item.
  GURL url;

  // A pointer to the menu_item. This is a weak reference in the GTK+ version
  // because the GtkMenu must sink the reference.
  GtkWidget* menu_item;

  // This ID is unique for a browser session and can be passed to the
  // TabRestoreService to re-open the closed window or tab that this
  // references. A non-0 session ID indicates that this is an entry can be
  // restored that way. Otherwise, the URL will be used to open the item and
  // this ID will be 0.
  SessionID::id_type session_id;

  // If the HistoryItem is a window, this will be the vector of tabs. Note
  // that this is a list of weak references. The |menu_item_map_| is the owner
  // of all items. If it is not a window, then the entry is a single page and
  // the vector will be empty.
  std::vector<HistoryItem*> tabs;

 private:
  DISALLOW_COPY_AND_ASSIGN(HistoryItem);
};

GlobalHistoryMenu::GlobalHistoryMenu(Browser* browser)
    : browser_(browser),
      profile_(browser_->profile()),
      history_menu_(NULL),
      top_sites_(NULL),
      tab_restore_service_(NULL),
      weak_ptr_factory_(this) {
}

GlobalHistoryMenu::~GlobalHistoryMenu() {
  if (tab_restore_service_)
    tab_restore_service_->RemoveObserver(this);

  STLDeleteContainerPairSecondPointers(menu_item_history_map_.begin(),
                                       menu_item_history_map_.end());
  menu_item_history_map_.clear();

  if (history_menu_) {
    gtk_widget_destroy(history_menu_);
    g_object_unref(history_menu_);
  }
}

void GlobalHistoryMenu::Init(GtkWidget* history_menu,
                             GtkWidget* history_menu_item) {
  history_menu_ = history_menu;
  g_object_ref_sink(history_menu_);

  // We have to connect to |history_menu_item|'s "activate" signal instead of
  // |history_menu|'s "show" signal because we are not supposed to modify the
  // menu during "show"
  g_signal_connect(history_menu_item, "activate",
                   G_CALLBACK(OnMenuActivateThunk), this);

  if (profile_) {
    top_sites_ = profile_->GetTopSites();
    if (top_sites_) {
      GetTopSitesData();

      // Register for notification when TopSites changes so that we can update
      // ourself.
      registrar_.Add(this, chrome::NOTIFICATION_TOP_SITES_CHANGED,
                     content::Source<history::TopSites>(top_sites_));
    }
  }
}

void GlobalHistoryMenu::GetTopSitesData() {
  DCHECK(top_sites_);

  top_sites_->GetMostVisitedURLs(
      base::Bind(&GlobalHistoryMenu::OnTopSitesReceived,
                 weak_ptr_factory_.GetWeakPtr()));
}

void GlobalHistoryMenu::OnTopSitesReceived(
    const history::MostVisitedURLList& visited_list) {
  ClearMenuSection(history_menu_, GlobalMenuBar::TAG_MOST_VISITED);

  int index = GetIndexOfMenuItemWithTag(
      history_menu_,
      GlobalMenuBar::TAG_MOST_VISITED_HEADER) + 1;

  for (size_t i = 0; i < visited_list.size() && i < kMostVisitedCount; ++i) {
    const history::MostVisitedURL& visited = visited_list[i];
    if (visited.url.spec().empty())
      break;  // This is the signal that there are no more real visited sites.

    HistoryItem* item = new HistoryItem();
    item->title = visited.title;
    item->url = visited.url;

    AddHistoryItemToMenu(item,
                         history_menu_,
                         GlobalMenuBar::TAG_MOST_VISITED,
                         index++);
  }
}

GlobalHistoryMenu::HistoryItem* GlobalHistoryMenu::HistoryItemForMenuItem(
    GtkWidget* menu_item) {
  MenuItemToHistoryMap::iterator it = menu_item_history_map_.find(menu_item);
  return it != menu_item_history_map_.end() ? it->second : NULL;
}

GlobalHistoryMenu::HistoryItem* GlobalHistoryMenu::HistoryItemForTab(
    const TabRestoreService::Tab& entry) {
  const sessions::SerializedNavigationEntry& current_navigation =
      entry.navigations.at(entry.current_navigation_index);
  HistoryItem* item = new HistoryItem();
  item->title = current_navigation.title();
  item->url = current_navigation.virtual_url();
  item->session_id = entry.id;

  return item;
}

GtkWidget* GlobalHistoryMenu::AddHistoryItemToMenu(HistoryItem* item,
                                                   GtkWidget* menu,
                                                   int tag,
                                                   int index) {
  string16 title = item->title;
  std::string url_string = item->url.possibly_invalid_spec();

  if (title.empty())
    title = UTF8ToUTF16(url_string);
  gfx::ElideString(title, kMaximumMenuWidthInChars, &title);

  GtkWidget* menu_item = gtk_menu_item_new_with_label(
      UTF16ToUTF8(title).c_str());

  item->menu_item = menu_item;
  gtk_widget_show(menu_item);
  g_object_set_data(G_OBJECT(menu_item), "type-tag", GINT_TO_POINTER(tag));
  g_signal_connect(menu_item, "activate",
                   G_CALLBACK(OnRecentlyClosedItemActivatedThunk), this);

  std::string tooltip = gtk_util::BuildTooltipTitleFor(item->title, item->url);
  gtk_widget_set_tooltip_markup(menu_item, tooltip.c_str());

  menu_item_history_map_.insert(std::make_pair(menu_item, item));
  gtk_menu_shell_insert(GTK_MENU_SHELL(menu), menu_item, index);

  return menu_item;
}

int GlobalHistoryMenu::GetIndexOfMenuItemWithTag(GtkWidget* menu, int tag_id) {
  GetIndexClosure closure;
  closure.found = false;
  closure.current_index = 0;
  closure.tag = tag_id;

  gtk_container_foreach(
      GTK_CONTAINER(menu),
      reinterpret_cast<void (*)(GtkWidget*, void*)>(GetIndexCallback),
      &closure);

  return closure.current_index;
}

void GlobalHistoryMenu::ClearMenuSection(GtkWidget* menu, int tag) {
  ClearMenuClosure closure;
  closure.container = menu;
  closure.menu_bar = this;
  closure.tag = tag;

  gtk_container_foreach(
      GTK_CONTAINER(menu),
      reinterpret_cast<void (*)(GtkWidget*, void*)>(ClearMenuCallback),
      &closure);
}

// static
void GlobalHistoryMenu::GetIndexCallback(GtkWidget* menu_item,
                                         GetIndexClosure* closure) {
  int tag = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menu_item), "type-tag"));
  if (tag == closure->tag)
    closure->found = true;

  if (!closure->found)
    closure->current_index++;
}

// static
void GlobalHistoryMenu::ClearMenuCallback(GtkWidget* menu_item,
                                          ClearMenuClosure* closure) {
  DCHECK_NE(closure->tag, 0);

  int tag = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(menu_item), "type-tag"));
  if (closure->tag == tag) {
    HistoryItem* item = closure->menu_bar->HistoryItemForMenuItem(menu_item);

    if (item) {
      closure->menu_bar->menu_item_history_map_.erase(menu_item);
      delete item;
    }

    GtkWidget* submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_item));
    if (submenu)
      closure->menu_bar->ClearMenuSection(submenu, closure->tag);

    gtk_container_remove(GTK_CONTAINER(closure->container), menu_item);
  }
}

void GlobalHistoryMenu::Observe(int type,
                                const content::NotificationSource& source,
                                const content::NotificationDetails& details) {
  if (type == chrome::NOTIFICATION_TOP_SITES_CHANGED) {
    GetTopSitesData();
  } else {
    NOTREACHED();
  }
}

void GlobalHistoryMenu::TabRestoreServiceChanged(TabRestoreService* service) {
  const TabRestoreService::Entries& entries = service->entries();

  ClearMenuSection(history_menu_, GlobalMenuBar::TAG_RECENTLY_CLOSED);

  // We'll get the index the "Recently Closed" header. (This can vary depending
  // on the number of "Most Visited" items.
  int index = GetIndexOfMenuItemWithTag(
      history_menu_,
      GlobalMenuBar::TAG_RECENTLY_CLOSED_HEADER) + 1;

  unsigned int added_count = 0;
  for (TabRestoreService::Entries::const_iterator it = entries.begin();
       it != entries.end() && added_count < kRecentlyClosedCount; ++it) {
    TabRestoreService::Entry* entry = *it;

    if (entry->type == TabRestoreService::WINDOW) {
      TabRestoreService::Window* entry_win =
          static_cast<TabRestoreService::Window*>(entry);
      std::vector<TabRestoreService::Tab>& tabs = entry_win->tabs;
      if (tabs.empty())
        continue;

      // Create the item for the parent/window.
      HistoryItem* item = new HistoryItem();
      item->session_id = entry_win->id;

      GtkWidget* submenu = gtk_menu_new();
      GtkWidget* restore_item = gtk_menu_item_new_with_label(
          l10n_util::GetStringUTF8(
              IDS_HISTORY_CLOSED_RESTORE_WINDOW_LINUX).c_str());
      g_object_set_data(G_OBJECT(restore_item), "type-tag",
                        GINT_TO_POINTER(GlobalMenuBar::TAG_RECENTLY_CLOSED));
      g_signal_connect(restore_item, "activate",
                       G_CALLBACK(OnRecentlyClosedItemActivatedThunk), this);
      gtk_widget_show(restore_item);

      // The mac version of this code allows the user to click on the parent
      // menu item to have the same effect as clicking the restore window
      // submenu item. GTK+ helpfully activates a menu item when it shows a
      // submenu so toss that feature out.
      menu_item_history_map_.insert(std::make_pair(restore_item, item));
      gtk_menu_shell_append(GTK_MENU_SHELL(submenu), restore_item);

      GtkWidget* separator = gtk_separator_menu_item_new();
      gtk_widget_show(separator);
      gtk_menu_shell_append(GTK_MENU_SHELL(submenu), separator);

      // Loop over the window's tabs and add them to the submenu.
      int subindex = 2;
      std::vector<TabRestoreService::Tab>::const_iterator iter;
      for (iter = tabs.begin(); iter != tabs.end(); ++iter) {
        TabRestoreService::Tab tab = *iter;
        HistoryItem* tab_item = HistoryItemForTab(tab);
        item->tabs.push_back(tab_item);
        AddHistoryItemToMenu(tab_item,
                             submenu,
                             GlobalMenuBar::TAG_RECENTLY_CLOSED,
                             subindex++);
      }

      std::string title = item->tabs.size() == 1 ?
          l10n_util::GetStringUTF8(
              IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_SINGLE) :
          l10n_util::GetStringFUTF8(
              IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_MULTIPLE,
              base::IntToString16(item->tabs.size()));

      // Create the menu item parent. Unlike mac, it's can't be activated.
      GtkWidget* parent_item = gtk_menu_item_new_with_label(title.c_str());
      gtk_widget_show(parent_item);
      g_object_set_data(G_OBJECT(parent_item), "type-tag",
                        GINT_TO_POINTER(GlobalMenuBar::TAG_RECENTLY_CLOSED));
      gtk_menu_item_set_submenu(GTK_MENU_ITEM(parent_item), submenu);

      gtk_menu_shell_insert(GTK_MENU_SHELL(history_menu_), parent_item,
                            index++);
      ++added_count;
    } else if (entry->type == TabRestoreService::TAB) {
      TabRestoreService::Tab* tab = static_cast<TabRestoreService::Tab*>(entry);
      HistoryItem* item = HistoryItemForTab(*tab);
      AddHistoryItemToMenu(item,
                           history_menu_,
                           GlobalMenuBar::TAG_RECENTLY_CLOSED,
                           index++);
      ++added_count;
    }
  }
}

void GlobalHistoryMenu::TabRestoreServiceDestroyed(
    TabRestoreService* service) {
  tab_restore_service_ = NULL;
}

void GlobalHistoryMenu::OnRecentlyClosedItemActivated(GtkWidget* sender) {
  WindowOpenDisposition disposition =
      event_utils::DispositionForCurrentButtonPressEvent();
  HistoryItem* item = HistoryItemForMenuItem(sender);

  // If this item can be restored using TabRestoreService, do so. Otherwise,
  // just load the URL.
  TabRestoreService* service =
      TabRestoreServiceFactory::GetForProfile(browser_->profile());
  if (item->session_id && service) {
    service->RestoreEntryById(browser_->tab_restore_service_delegate(),
                              item->session_id, browser_->host_desktop_type(),
                              UNKNOWN);
  } else {
    DCHECK(item->url.is_valid());
    browser_->OpenURL(OpenURLParams(item->url, content::Referrer(), disposition,
                      content::PAGE_TRANSITION_AUTO_BOOKMARK, false));
  }
}

void GlobalHistoryMenu::OnMenuActivate(GtkWidget* sender) {
  if (!tab_restore_service_) {
    tab_restore_service_ = TabRestoreServiceFactory::GetForProfile(profile_);
    if (tab_restore_service_) {
      tab_restore_service_->LoadTabsFromLastSession();
      tab_restore_service_->AddObserver(this);

      // If LoadTabsFromLastSession doesn't load tabs, it won't call
      // TabRestoreServiceChanged(). This ensures that all new windows after
      // the first one will have their menus populated correctly.
      TabRestoreServiceChanged(tab_restore_service_);
    }
  }
}