// 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 <algorithm>

#include "base/basictypes.h"
#include "base/compiler_specific.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/metrics/histogram.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/protector/base_setting_change.h"
#include "chrome/browser/protector/histograms.h"
#include "chrome/browser/protector/protector_service.h"
#include "chrome/browser/protector/protector_service_factory.h"
#include "chrome/browser/search_engines/template_url.h"
#include "chrome/browser/search_engines/template_url_prepopulate_data.h"
#include "chrome/browser/search_engines/template_url_service.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/search_engines/template_url_service_observer.h"
#include "chrome/browser/webdata/keyword_table.h"
#include "chrome/common/chrome_notification_types.h"
#include "chrome/common/url_constants.h"
#include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_registrar.h"
#include "content/public/browser/notification_service.h"
#include "googleurl/src/gurl.h"
#include "grit/chromium_strings.h"
#include "grit/generated_resources.h"
#include "grit/theme_resources.h"
#include "ui/base/l10n/l10n_util.h"

namespace protector {

namespace {

// Maximum length of the search engine name to be displayed.
const size_t kMaxDisplayedNameLength = 16;

// Matches TemplateURL with all fields set from the prepopulated data equal
// to fields in another TemplateURL.
class TemplateURLIsSame {
 public:
  // Creates a matcher based on |other|.
  explicit TemplateURLIsSame(const TemplateURL* other) : other_(other) {
  }

  // Returns true if both |other| and |url| are NULL or have same field values.
  bool operator()(const TemplateURL* url) {
    if (url == other_ )
      return true;
    if (!url || !other_)
      return false;
    return url->short_name() == other_->short_name() &&
        url->HasSameKeywordAs(*other_) &&
        url->url() == other_->url() &&
        url->suggestions_url() == other_->suggestions_url() &&
        url->instant_url() == other_->instant_url() &&
        url->safe_for_autoreplace() == other_->safe_for_autoreplace() &&
        url->favicon_url() == other_->favicon_url() &&
        url->show_in_default_list() == other_->show_in_default_list() &&
        url->input_encodings() == other_->input_encodings() &&
        url->prepopulate_id() == other_->prepopulate_id();
  }

 private:
  const TemplateURL* other_;
};

}  // namespace

// Default search engine change tracked by Protector.
class DefaultSearchProviderChange : public BaseSettingChange,
                                    public TemplateURLServiceObserver,
                                    public content::NotificationObserver {
 public:
  DefaultSearchProviderChange(TemplateURL* new_search_provider,
                              TemplateURL* backup_search_provider);

  // BaseSettingChange overrides:
  virtual bool Init(Profile* profile) OVERRIDE;
  virtual void InitWhenDisabled(Profile* profile) OVERRIDE;
  virtual void Apply(Browser* browser) OVERRIDE;
  virtual void Discard(Browser* browser) OVERRIDE;
  virtual void Timeout() OVERRIDE;
  virtual int GetBadgeIconID() const OVERRIDE;
  virtual int GetMenuItemIconID() const OVERRIDE;
  virtual int GetBubbleIconID() const OVERRIDE;
  virtual string16 GetBubbleTitle() const OVERRIDE;
  virtual string16 GetBubbleMessage() const OVERRIDE;
  virtual string16 GetApplyButtonText() const OVERRIDE;
  virtual string16 GetDiscardButtonText() const OVERRIDE;
  virtual DisplayName GetApplyDisplayName() const OVERRIDE;
  virtual GURL GetNewSettingURL() const OVERRIDE;

 private:
  virtual ~DefaultSearchProviderChange();

  // TemplateURLServiceObserver overrides:
  virtual void OnTemplateURLServiceChanged() OVERRIDE;

  // content::NotificationObserver overrides:
  virtual void Observe(int type,
                       const content::NotificationSource& source,
                       const content::NotificationDetails& details) OVERRIDE;

  // Sets the default search provider to |*search_provider|. If a matching
  // TemplateURL already exists, it is reused and |*search_provider| is reset.
  // Otherwise, adds |*search_provider| to keywords and releases it.
  // In both cases, |*search_provider| is NULL after this call.
  // Returns the new default search provider (either |*search_provider| or the
  // reused TemplateURL).
  const TemplateURL* SetDefaultSearchProvider(
      scoped_ptr<TemplateURL>* search_provider);

  // Returns true if |new_search_provider_| can be used as the default search
  // provider.
  bool NewSearchProviderValid() const;

  // Opens the Search engine settings page in a new tab.
  void OpenSearchEngineSettings(Browser* browser);

  // Returns the TemplateURLService instance for the Profile this change is
  // related to.
  TemplateURLService* GetTemplateURLService();

  // Histogram ID of the new search provider.
  int new_histogram_id_;
  // Indicates that the default search was restored to the prepopulated default
  // search engines.
  bool is_fallback_;
  // Indicates that the the prepopulated default search is the same as
  // |new_search_provider_|.
  bool fallback_is_new_;
  // ID of |new_search_provider_|.
  TemplateURLID new_id_;
  // The default search at the moment the change was detected. Will be used to
  // restore the new default search back if Apply is called. Will be set to
  // |NULL| if removed from the TemplateURLService.
  TemplateURL* new_search_provider_;
  // Default search provider set by Init for the period until user makes a
  // choice and either Apply or Discard is performed. Never is |NULL| during
  // that period since the change will dismiss itself if this provider gets
  // deleted or stops being the default.
  const TemplateURL* default_search_provider_;
  // Stores backup of the default search until it becomes owned by the
  // TemplateURLService or deleted.
  scoped_ptr<TemplateURL> backup_search_provider_;
  content::NotificationRegistrar registrar_;

  DISALLOW_COPY_AND_ASSIGN(DefaultSearchProviderChange);
};

DefaultSearchProviderChange::DefaultSearchProviderChange(
    TemplateURL* new_search_provider,
    TemplateURL* backup_search_provider)
    : new_histogram_id_(GetSearchProviderHistogramID(new_search_provider)),
      is_fallback_(false),
      fallback_is_new_(false),
      new_search_provider_(new_search_provider),
      default_search_provider_(NULL),
      backup_search_provider_(backup_search_provider) {
  if (backup_search_provider) {
    UMA_HISTOGRAM_ENUMERATION(
        kProtectorHistogramSearchProviderHijacked,
        new_histogram_id_,
        kProtectorMaxSearchProviderID);
  } else {
    UMA_HISTOGRAM_ENUMERATION(
        kProtectorHistogramSearchProviderCorrupt,
        new_histogram_id_,
        kProtectorMaxSearchProviderID);
  }
}

DefaultSearchProviderChange::~DefaultSearchProviderChange() {
  if (profile())
    GetTemplateURLService()->RemoveObserver(this);
}

bool DefaultSearchProviderChange::Init(Profile* profile) {
  if (!BaseSettingChange::Init(profile))
    return false;

  if (!backup_search_provider_.get() ||
      !backup_search_provider_->SupportsReplacement()) {
    // Fallback to a prepopulated default search provider, ignoring any
    // overrides in Prefs.
    backup_search_provider_.reset(
        TemplateURLPrepopulateData::GetPrepopulatedDefaultSearch(NULL));
    is_fallback_ = true;
    VLOG(1) << "Fallback search provider: "
            << backup_search_provider_->short_name();
  }

  default_search_provider_ = SetDefaultSearchProvider(&backup_search_provider_);
  DCHECK(default_search_provider_);
  // |backup_search_provider_| should be |NULL| since now.
  DCHECK(!backup_search_provider_.get());
  if (is_fallback_ && default_search_provider_ == new_search_provider_)
    fallback_is_new_ = true;

  int restored_histogram_id =
      GetSearchProviderHistogramID(default_search_provider_);
  UMA_HISTOGRAM_ENUMERATION(
      kProtectorHistogramSearchProviderRestored,
      restored_histogram_id,
      kProtectorMaxSearchProviderID);

  if (is_fallback_) {
    VLOG(1) << "Fallback to search provider: "
            << default_search_provider_->short_name();
    UMA_HISTOGRAM_ENUMERATION(
        kProtectorHistogramSearchProviderFallback,
        restored_histogram_id,
        kProtectorMaxSearchProviderID);
  }

  // Listen for the default search provider changes.
  GetTemplateURLService()->AddObserver(this);

  if (new_search_provider_) {
    // Listen for removal of |new_search_provider_|.
    new_id_ = new_search_provider_->id();
    registrar_.Add(
        this, chrome::NOTIFICATION_TEMPLATE_URL_REMOVED,
        content::Source<Profile>(profile->GetOriginalProfile()));
  }

  return true;
}

void DefaultSearchProviderChange::InitWhenDisabled(Profile* profile) {
  // The --no-protector case is handled in TemplateURLService internals.
  // TODO(ivankr): move it here.
  NOTREACHED();
}

void DefaultSearchProviderChange::Apply(Browser* browser) {
  UMA_HISTOGRAM_ENUMERATION(
      kProtectorHistogramSearchProviderApplied,
      new_histogram_id_,
      kProtectorMaxSearchProviderID);

  GetTemplateURLService()->RemoveObserver(this);
  if (NewSearchProviderValid()) {
    GetTemplateURLService()->SetDefaultSearchProvider(new_search_provider_);
  } else {
    // Open settings page in case the new setting is invalid.
    OpenSearchEngineSettings(browser);
  }
}

void DefaultSearchProviderChange::Discard(Browser* browser) {
  UMA_HISTOGRAM_ENUMERATION(
      kProtectorHistogramSearchProviderDiscarded,
      new_histogram_id_,
      kProtectorMaxSearchProviderID);

  GetTemplateURLService()->RemoveObserver(this);
  if (is_fallback_) {
    // Open settings page in case the old setting is invalid.
    OpenSearchEngineSettings(browser);
  }
  // Nothing to do otherwise since we have already set the search engine
  // to |old_id_| in |Init|.
}

void DefaultSearchProviderChange::Timeout() {
  UMA_HISTOGRAM_ENUMERATION(
      kProtectorHistogramSearchProviderTimeout,
      new_histogram_id_,
      kProtectorMaxSearchProviderID);
}

int DefaultSearchProviderChange::GetBadgeIconID() const {
  return IDR_SEARCH_ENGINE_CHANGE_BADGE;
}

int DefaultSearchProviderChange::GetMenuItemIconID() const {
  return IDR_SEARCH_ENGINE_CHANGE_MENU;
}

int DefaultSearchProviderChange::GetBubbleIconID() const {
  return IDR_SEARCH_ENGINE_CHANGE_ALERT;
}

string16 DefaultSearchProviderChange::GetBubbleTitle() const {
  return l10n_util::GetStringUTF16(IDS_SEARCH_ENGINE_CHANGE_TITLE);
}

string16 DefaultSearchProviderChange::GetBubbleMessage() const {
  if (is_fallback_) {
    return l10n_util::GetStringFUTF16(
        IDS_SEARCH_ENGINE_CHANGE_NO_BACKUP_MESSAGE,
        default_search_provider_->short_name());
  }
  return l10n_util::GetStringUTF16(IDS_SEARCH_ENGINE_CHANGE_MESSAGE);
}

string16 DefaultSearchProviderChange::GetApplyButtonText() const {
  if (NewSearchProviderValid()) {
    // If backup search engine is lost and fallback was made to the current
    // search provider then there is no need to show this button.
    if (fallback_is_new_)
      return string16();
    string16 name = new_search_provider_->short_name();
    return (name.length() > kMaxDisplayedNameLength) ?
        l10n_util::GetStringUTF16(IDS_CHANGE_SEARCH_ENGINE_NO_NAME) :
        l10n_util::GetStringFUTF16(IDS_CHANGE_SEARCH_ENGINE, name);
  }
  if (is_fallback_) {
    // Both settings are lost: don't show this button.
    return string16();
  }
  // New setting is lost, offer to go to settings.
  return l10n_util::GetStringUTF16(IDS_SELECT_SEARCH_ENGINE);
}

string16 DefaultSearchProviderChange::GetDiscardButtonText() const {
  if (!is_fallback_) {
    string16 name = default_search_provider_->short_name();
    return (name.length() > kMaxDisplayedNameLength) ?
        l10n_util::GetStringUTF16(IDS_KEEP_SETTING) :
        l10n_util::GetStringFUTF16(IDS_KEEP_SEARCH_ENGINE, name);
  }
  // Old setting is lost, offer to go to settings.
  return l10n_util::GetStringUTF16(IDS_SELECT_SEARCH_ENGINE);
}

BaseSettingChange::DisplayName
DefaultSearchProviderChange::GetApplyDisplayName() const {
  if (NewSearchProviderValid() &&
      new_search_provider_->short_name().length() <= kMaxDisplayedNameLength) {
    return DisplayName(kDefaultSearchProviderChangeNamePriority,
                       new_search_provider_->short_name());
  }
  // Return the default empty name.
  return BaseSettingChange::GetApplyDisplayName();
}

GURL DefaultSearchProviderChange::GetNewSettingURL() const {
  if (is_fallback_) {
    // Do not collide this change when fallback was made, since the message
    // and Discard behaviour is pretty different.
    return GURL();
  }
  if (!NewSearchProviderValid())
    return GURL();
  return TemplateURLService::GenerateSearchURL(new_search_provider_);
}

void DefaultSearchProviderChange::OnTemplateURLServiceChanged() {
  TemplateURLService* url_service = GetTemplateURLService();
  if (url_service->GetDefaultSearchProvider() != default_search_provider_) {
    VLOG(1) << "Default search provider has been changed by user";
    default_search_provider_ = NULL;
    url_service->RemoveObserver(this);
    // Will delete this DefaultSearchProviderChange instance.
    ProtectorServiceFactory::GetForProfile(profile())->DismissChange(this);
  }
}

void DefaultSearchProviderChange::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  DCHECK(type == chrome::NOTIFICATION_TEMPLATE_URL_REMOVED);
  TemplateURLID id = *content::Details<TemplateURLID>(details).ptr();
  if (id == new_id_)
    new_search_provider_ = NULL;
  registrar_.Remove(this, type, source);
  // TODO(ivankr): should the change be dismissed as well? Probably not,
  // since this may happend due to Sync or whatever. In that case, if user
  // clicks on 'Change to...', the Search engine settings page will be opened.
}

const TemplateURL* DefaultSearchProviderChange::SetDefaultSearchProvider(
    scoped_ptr<TemplateURL>* search_provider) {
  TemplateURLService* url_service = GetTemplateURLService();
  TemplateURLService::TemplateURLVector urls = url_service->GetTemplateURLs();
  TemplateURL* new_default_provider = NULL;

  // Check if this provider already exists and add it otherwise.
  TemplateURLService::TemplateURLVector::const_iterator i =
      std::find_if(urls.begin(), urls.end(),
                   TemplateURLIsSame(search_provider->get()));
  if (i != urls.end()) {
    VLOG(1) << "Provider already exists";
    new_default_provider = *i;
    search_provider->reset();
  } else {
    VLOG(1) << "No match, adding new provider";
    new_default_provider = search_provider->get();
    url_service->Add(search_provider->release());
    UMA_HISTOGRAM_ENUMERATION(
        kProtectorHistogramSearchProviderMissing,
        GetSearchProviderHistogramID(new_default_provider),
        kProtectorMaxSearchProviderID);
  }

  // TODO(ivankr): handle keyword conflicts with existing providers.
  DCHECK(new_default_provider);
  VLOG(1) << "Default search provider set to: "
          << new_default_provider->short_name();
  url_service->SetDefaultSearchProvider(new_default_provider);
  return new_default_provider;
}

bool DefaultSearchProviderChange::NewSearchProviderValid() const {
  return new_search_provider_ && new_search_provider_->SupportsReplacement();
}

void DefaultSearchProviderChange::OpenSearchEngineSettings(Browser* browser) {
  ProtectorServiceFactory::GetForProfile(profile())->OpenTab(
      GURL(std::string(chrome::kChromeUISettingsURL) +
           chrome::kSearchEnginesSubPage), browser);
}

TemplateURLService* DefaultSearchProviderChange::GetTemplateURLService() {
  TemplateURLService* url_service =
      TemplateURLServiceFactory::GetForProfile(profile());
  DCHECK(url_service);
  return url_service;
}

BaseSettingChange* CreateDefaultSearchProviderChange(TemplateURL* actual,
                                                     TemplateURL* backup) {
  return new DefaultSearchProviderChange(actual, backup);
}

}  // namespace protector