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

#include "chrome/browser/extensions/browser_event_router.h"
#include "chrome/browser/extensions/extension_prefs.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/tab_contents/tab_contents.h"
#include "chrome/common/chrome_notification_types.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/extensions/extension_switch_utils.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/web_contents.h"

using extensions::Extension;
using extensions::ExtensionList;

namespace {

// Returns true if an |extension| is in an |extension_list|.
bool IsInExtensionList(const Extension* extension,
                       const extensions::ExtensionList& extension_list) {
  for (size_t i = 0; i < extension_list.size(); i++) {
    if (extension_list[i].get() == extension)
      return true;
  }
  return false;
}

}  // namespace

ExtensionToolbarModel::ExtensionToolbarModel(ExtensionService* service)
    : service_(service),
      prefs_(service->profile()->GetPrefs()),
      extensions_initialized_(false) {
  DCHECK(service_);

  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED,
                 content::Source<Profile>(service_->profile()));
  registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNLOADED,
                 content::Source<Profile>(service_->profile()));
  registrar_.Add(this, chrome::NOTIFICATION_EXTENSIONS_READY,
                 content::Source<Profile>(service_->profile()));
  registrar_.Add(
      this, chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED,
      content::Source<extensions::ExtensionPrefs>(service_->extension_prefs()));

  visible_icon_count_ = prefs_->GetInteger(prefs::kExtensionToolbarSize);
}

ExtensionToolbarModel::~ExtensionToolbarModel() {
}

void ExtensionToolbarModel::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void ExtensionToolbarModel::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void ExtensionToolbarModel::MoveBrowserAction(const Extension* extension,
                                              int index) {
  ExtensionList::iterator pos = std::find(toolbar_items_.begin(),
      toolbar_items_.end(), extension);
  if (pos == toolbar_items_.end()) {
    NOTREACHED();
    return;
  }
  toolbar_items_.erase(pos);

  int i = 0;
  bool inserted = false;
  for (ExtensionList::iterator iter = toolbar_items_.begin();
       iter != toolbar_items_.end();
       ++iter, ++i) {
    if (i == index) {
      toolbar_items_.insert(iter, make_scoped_refptr(extension));
      inserted = true;
      break;
    }
  }

  if (!inserted) {
    DCHECK_EQ(index, static_cast<int>(toolbar_items_.size()));
    index = toolbar_items_.size();

    toolbar_items_.push_back(make_scoped_refptr(extension));
  }

  FOR_EACH_OBSERVER(Observer, observers_, BrowserActionMoved(extension, index));

  UpdatePrefs();
}

ExtensionToolbarModel::Action ExtensionToolbarModel::ExecuteBrowserAction(
    const Extension* extension,
    Browser* browser,
    GURL* popup_url_out) {
  TabContents* tab_contents = chrome::GetActiveTabContents(browser);
  if (!tab_contents)
    return ACTION_NONE;

  int tab_id = ExtensionTabUtil::GetTabId(tab_contents->web_contents());
  if (tab_id < 0)
    return ACTION_NONE;

  ExtensionAction* browser_action = extension->browser_action();

  // For browser actions, visibility == enabledness.
  if (!browser_action->GetIsVisible(tab_id))
    return ACTION_NONE;

  extensions::TabHelper::FromWebContents(tab_contents->web_contents())->
      active_tab_permission_granter()->GrantIfRequested(extension);

  if (browser_action->HasPopup(tab_id)) {
    if (popup_url_out)
      *popup_url_out = browser_action->GetPopupUrl(tab_id);
    return ACTION_SHOW_POPUP;
  }

  service_->browser_event_router()->BrowserActionExecuted(
      *browser_action, browser);
  return ACTION_NONE;
}

void ExtensionToolbarModel::SetVisibleIconCount(int count) {
  visible_icon_count_ =
      count == static_cast<int>(toolbar_items_.size()) ? -1 : count;
  prefs_->SetInteger(prefs::kExtensionToolbarSize, visible_icon_count_);
}

void ExtensionToolbarModel::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  if (type == chrome::NOTIFICATION_EXTENSIONS_READY) {
    InitializeExtensionLists();
    return;
  }

  if (!service_->is_ready())
    return;

  const Extension* extension = NULL;
  if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) {
    extension = content::Details<extensions::UnloadedExtensionInfo>(
        details)->extension;
  } else {
    extension = content::Details<const Extension>(details).ptr();
  }
  ExtensionList* list_with_extension = FindListWithExtension(extension);
  if (type == chrome::NOTIFICATION_EXTENSION_LOADED) {
    // We don't want to add the same extension twice. It may have already been
    // added by EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED below, if the user
    // hides the browser action and then disables and enables the extension.
    if (list_with_extension)
      return;
    if (extensions::switch_utils::IsExtensionsInActionBoxEnabled())
      AddExtension(extension, &action_box_menu_items_);
    else if (service_->extension_prefs()->GetBrowserActionVisibility(extension))
      AddExtension(extension, &toolbar_items_);
  } else if (type == chrome::NOTIFICATION_EXTENSION_UNLOADED) {
    if (list_with_extension)
      RemoveExtension(extension, list_with_extension);
  } else if (type ==
      chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED) {
    if (extensions::switch_utils::IsExtensionsInActionBoxEnabled()) {
      // TODO(yefim): Implement this when implementing drag & drop
      // for action box menu.
    } else if (
      service_->extension_prefs()->GetBrowserActionVisibility(extension)) {
      AddExtension(extension, &toolbar_items_);
    } else {
      RemoveExtension(extension, &toolbar_items_);
    }
  } else {
    NOTREACHED() << "Received unexpected notification";
  }
}

void ExtensionToolbarModel::AddExtension(const Extension* extension,
                                         ExtensionList* list) {
  // We only care about extensions with browser actions.
  if (!extension->browser_action())
    return;

  if (extension->id() == last_extension_removed_ &&
      last_extension_removed_index_ < list->size()) {
    list->insert(list->begin() + last_extension_removed_index_,
                 make_scoped_refptr(extension));
    // TODO: figure out the right long term solution.
    if (list == &toolbar_items_) {
      FOR_EACH_OBSERVER(Observer, observers_,
          BrowserActionAdded(extension, last_extension_removed_index_));
    }
  } else {
    list->push_back(make_scoped_refptr(extension));
    // TODO: figure out the right long term solution.
    if (list == &toolbar_items_) {
      FOR_EACH_OBSERVER(Observer, observers_,
                        BrowserActionAdded(extension, list->size() - 1));
    }
  }

  last_extension_removed_ = "";
  last_extension_removed_index_ = -1;

  UpdatePrefs();
}

void ExtensionToolbarModel::RemoveExtension(const Extension* extension,
                                            ExtensionList* list) {
  ExtensionList::iterator pos =
      std::find(list->begin(), list->end(), extension);
  if (pos == list->end())
    return;

  last_extension_removed_ = extension->id();
  last_extension_removed_index_ = pos - list->begin();

  list->erase(pos);
  // TODO: figure out the right long term solution.
  if (list == &toolbar_items_) {
    FOR_EACH_OBSERVER(Observer, observers_,
                      BrowserActionRemoved(extension));
  }

  UpdatePrefs();
}

extensions::ExtensionList* ExtensionToolbarModel::FindListWithExtension(
    const Extension* extension) {
  if (IsInExtensionList(extension, toolbar_items_))
    return &toolbar_items_;
  return IsInExtensionList(extension, action_box_menu_items_) ?
      &action_box_menu_items_ : NULL;
}


// Combine the currently enabled extensions that have browser actions (which
// we get from the ExtensionService) with the ordering we get from the
// pref service. For robustness we use a somewhat inefficient process:
// 1. Create a vector of extensions sorted by their pref values. This vector may
// have holes.
// 2. Create a vector of extensions that did not have a pref value.
// 3. Remove holes from the sorted vector and append the unsorted vector.
void ExtensionToolbarModel::InitializeExtensionLists() {
  DCHECK(service_->is_ready());

  if (extensions::switch_utils::IsExtensionsInActionBoxEnabled())
    PopulateForActionBoxMode();
  else
    PopulateForNonActionBoxMode();

  UpdatePrefs();

  extensions_initialized_ = true;
  FOR_EACH_OBSERVER(Observer, observers_, ModelLoaded());
}

void ExtensionToolbarModel::PopulateForActionBoxMode() {
  const extensions::ExtensionPrefs::ExtensionIds toolbar_order =
      service_->extension_prefs()->GetToolbarOrder();
  extensions::ExtensionPrefs::ExtensionIds action_box_order =
      service_->extension_prefs()->GetActionBoxOrder();

  // Add all browser actions not already in the toolbar or action box
  // to the action box (the prefs list may omit some extensions).
  for (ExtensionSet::const_iterator iter = service_->extensions()->begin();
       iter != service_->extensions()->end(); ++iter) {
    const Extension* extension = *iter;
    if (!extension->browser_action())
      continue;

    if (std::find(toolbar_order.begin(), toolbar_order.end(),
                  extension->id()) != toolbar_order.end())
      continue;

    if (std::find(action_box_order.begin(), action_box_order.end(),
                  extension->id()) != action_box_order.end())
      continue;

    action_box_order.push_back(extension->id());
  }

  FillExtensionList(action_box_order, &action_box_menu_items_);
  FillExtensionList(toolbar_order, &toolbar_items_);
}

void ExtensionToolbarModel::PopulateForNonActionBoxMode() {
  const extensions::ExtensionPrefs::ExtensionIds pref_order =
      service_->extension_prefs()->GetToolbarOrder();
  // Items that have a pref for their position.
  ExtensionList sorted;
  sorted.resize(pref_order.size(), NULL);
  // The items that don't have a pref for their position.
  ExtensionList unsorted;

  // Create the lists.
  for (ExtensionSet::const_iterator it = service_->extensions()->begin();
       it != service_->extensions()->end(); ++it) {
    const Extension* extension = *it;
    if (!extension->browser_action())
      continue;
    if (!service_->extension_prefs()->GetBrowserActionVisibility(extension))
      continue;

    extensions::ExtensionPrefs::ExtensionIds::const_iterator pos =
        std::find(pref_order.begin(), pref_order.end(), extension->id());
    if (pos != pref_order.end())
      sorted[pos - pref_order.begin()] = extension;
    else
      unsorted.push_back(make_scoped_refptr(extension));
  }

  // Merge the lists.
  toolbar_items_.clear();
  toolbar_items_.reserve(sorted.size() + unsorted.size());
  for (ExtensionList::const_iterator iter = sorted.begin();
       iter != sorted.end(); ++iter) {
    // It's possible for the extension order to contain items that aren't
    // actually loaded on this machine.  For example, when extension sync is on,
    // we sync the extension order as-is but double-check with the user before
    // syncing NPAPI-containing extensions, so if one of those is not actually
    // synced, we'll get a NULL in the list.  This sort of case can also happen
    // if some error prevents an extension from loading.
    if (*iter != NULL)
      toolbar_items_.push_back(*iter);
  }
  toolbar_items_.insert(toolbar_items_.end(), unsorted.begin(),
                        unsorted.end());

  // Inform observers.
  for (size_t i = 0; i < toolbar_items_.size(); i++) {
    FOR_EACH_OBSERVER(Observer, observers_,
                      BrowserActionAdded(toolbar_items_[i], i));
  }
}

void ExtensionToolbarModel::FillExtensionList(
    const extensions::ExtensionPrefs::ExtensionIds& order,
    ExtensionList* list) {
  list->clear();
  list->reserve(order.size());
  for (size_t i = 0; i < order.size(); ++i) {
    const extensions::Extension* extension =
        service_->GetExtensionById(order[i], false);
    if (extension)
      AddExtension(extension, list);
  }
}

void ExtensionToolbarModel::UpdatePrefs() {
  if (!service_->extension_prefs())
    return;

  extensions::ExtensionPrefs::ExtensionIds toolbar_ids;
  toolbar_ids.reserve(toolbar_items_.size());
  for (size_t i = 0; i < toolbar_items_.size(); ++i)
    toolbar_ids.push_back(toolbar_items_[i]->id());
  service_->extension_prefs()->SetToolbarOrder(toolbar_ids);

  extensions::ExtensionPrefs::ExtensionIds action_box_ids;
  action_box_ids.reserve(action_box_menu_items_.size());
  for (size_t i = 0; i < action_box_menu_items_.size(); ++i)
    action_box_ids.push_back(action_box_menu_items_[i]->id());
  service_->extension_prefs()->SetActionBoxOrder(action_box_ids);
}

int ExtensionToolbarModel::IncognitoIndexToOriginal(int incognito_index) {
  int original_index = 0, i = 0;
  for (ExtensionList::iterator iter = toolbar_items_.begin();
       iter != toolbar_items_.end();
       ++iter, ++original_index) {
    if (service_->IsIncognitoEnabled((*iter)->id())) {
      if (incognito_index == i)
        break;
      ++i;
    }
  }
  return original_index;
}

int ExtensionToolbarModel::OriginalIndexToIncognito(int original_index) {
  int incognito_index = 0, i = 0;
  for (ExtensionList::iterator iter = toolbar_items_.begin();
       iter != toolbar_items_.end();
       ++iter, ++i) {
    if (original_index == i)
      break;
    if (service_->IsIncognitoEnabled((*iter)->id()))
      ++incognito_index;
  }
  return incognito_index;
}