// Copyright 2013 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/api/messaging/incognito_connectability.h"

#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/infobars/infobar_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/grit/generated_resources.h"
#include "components/infobars/core/confirm_infobar_delegate.h"
#include "components/infobars/core/infobar.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/extension.h"
#include "ui/base/l10n/l10n_util.h"

using infobars::InfoBar;
using infobars::InfoBarManager;

namespace extensions {

namespace {

IncognitoConnectability::ScopedAlertTracker::Mode g_alert_mode =
    IncognitoConnectability::ScopedAlertTracker::INTERACTIVE;
int g_alert_count = 0;

class IncognitoConnectabilityInfoBarDelegate : public ConfirmInfoBarDelegate {
 public:
  typedef base::Callback<void(
      IncognitoConnectability::ScopedAlertTracker::Mode)> InfoBarCallback;

  // Creates a confirmation infobar and delegate and adds the infobar to
  // |infobar_service|.
  static InfoBar* Create(InfoBarManager* infobar_manager,
                         const base::string16& message,
                         const InfoBarCallback& callback);

  // Marks the infobar as answered so that the callback is not executed when the
  // delegate is destroyed.
  void set_answered() { answered_ = true; }

 private:
  IncognitoConnectabilityInfoBarDelegate(const base::string16& message,
                                         const InfoBarCallback& callback);
  ~IncognitoConnectabilityInfoBarDelegate() override;

  // ConfirmInfoBarDelegate:
  Type GetInfoBarType() const override;
  infobars::InfoBarDelegate::InfoBarIdentifier GetIdentifier() const override;
  base::string16 GetMessageText() const override;
  base::string16 GetButtonLabel(InfoBarButton button) const override;
  bool Accept() override;
  bool Cancel() override;

  base::string16 message_;
  bool answered_;
  InfoBarCallback callback_;
};

// static
InfoBar* IncognitoConnectabilityInfoBarDelegate::Create(
    InfoBarManager* infobar_manager,
    const base::string16& message,
    const IncognitoConnectabilityInfoBarDelegate::InfoBarCallback& callback) {
  return infobar_manager->AddInfoBar(infobar_manager->CreateConfirmInfoBar(
      scoped_ptr<ConfirmInfoBarDelegate>(
          new IncognitoConnectabilityInfoBarDelegate(message, callback))));
}

IncognitoConnectabilityInfoBarDelegate::IncognitoConnectabilityInfoBarDelegate(
    const base::string16& message,
    const InfoBarCallback& callback)
    : message_(message), answered_(false), callback_(callback) {
}

IncognitoConnectabilityInfoBarDelegate::
    ~IncognitoConnectabilityInfoBarDelegate() {
  if (!answered_) {
    // The infobar has closed without the user expressing an explicit
    // preference. The current request should be denied but further requests
    // should show an interactive prompt.
    callback_.Run(IncognitoConnectability::ScopedAlertTracker::INTERACTIVE);
  }
}

infobars::InfoBarDelegate::Type
IncognitoConnectabilityInfoBarDelegate::GetInfoBarType() const {
  return PAGE_ACTION_TYPE;
}

infobars::InfoBarDelegate::InfoBarIdentifier
IncognitoConnectabilityInfoBarDelegate::GetIdentifier() const {
  return INCOGNITO_CONNECTABILITY_INFOBAR_DELEGATE;
}

base::string16 IncognitoConnectabilityInfoBarDelegate::GetMessageText() const {
  return message_;
}

base::string16 IncognitoConnectabilityInfoBarDelegate::GetButtonLabel(
    InfoBarButton button) const {
  return l10n_util::GetStringUTF16(
      (button == BUTTON_OK) ? IDS_PERMISSION_ALLOW : IDS_PERMISSION_DENY);
}

bool IncognitoConnectabilityInfoBarDelegate::Accept() {
  callback_.Run(IncognitoConnectability::ScopedAlertTracker::ALWAYS_ALLOW);
  answered_ = true;
  return true;
}

bool IncognitoConnectabilityInfoBarDelegate::Cancel() {
  callback_.Run(IncognitoConnectability::ScopedAlertTracker::ALWAYS_DENY);
  answered_ = true;
  return true;
}

}  // namespace

IncognitoConnectability::ScopedAlertTracker::ScopedAlertTracker(Mode mode)
    : last_checked_invocation_count_(g_alert_count) {
  DCHECK_EQ(INTERACTIVE, g_alert_mode);
  DCHECK_NE(INTERACTIVE, mode);
  g_alert_mode = mode;
}

IncognitoConnectability::ScopedAlertTracker::~ScopedAlertTracker() {
  DCHECK_NE(INTERACTIVE, g_alert_mode);
  g_alert_mode = INTERACTIVE;
}

int IncognitoConnectability::ScopedAlertTracker::GetAndResetAlertCount() {
  int result = g_alert_count - last_checked_invocation_count_;
  last_checked_invocation_count_ = g_alert_count;
  return result;
}

IncognitoConnectability::IncognitoConnectability(
    content::BrowserContext* context)
    : weak_factory_(this) {
  CHECK(context->IsOffTheRecord());
}

IncognitoConnectability::~IncognitoConnectability() {
}

// static
IncognitoConnectability* IncognitoConnectability::Get(
    content::BrowserContext* context) {
  return BrowserContextKeyedAPIFactory<IncognitoConnectability>::Get(context);
}

void IncognitoConnectability::Query(
    const Extension* extension,
    content::WebContents* web_contents,
    const GURL& url,
    const base::Callback<void(bool)>& callback) {
  GURL origin = url.GetOrigin();
  if (origin.is_empty()) {
    callback.Run(false);
    return;
  }

  if (IsInMap(extension, origin, allowed_origins_)) {
    callback.Run(true);
    return;
  }

  if (IsInMap(extension, origin, disallowed_origins_)) {
    callback.Run(false);
    return;
  }

  PendingOrigin& pending_origin =
      pending_origins_[make_pair(extension->id(), origin)];
  InfoBarManager* infobar_manager =
      InfoBarService::FromWebContents(web_contents);
  TabContext& tab_context = pending_origin[infobar_manager];
  tab_context.callbacks.push_back(callback);
  if (tab_context.infobar) {
    // This tab is already displaying an infobar for this extension and origin.
    return;
  }

  // We need to ask the user.
  ++g_alert_count;

  switch (g_alert_mode) {
    // Production code should always be using INTERACTIVE.
    case ScopedAlertTracker::INTERACTIVE: {
      int template_id =
          extension->is_app()
              ? IDS_EXTENSION_PROMPT_APP_CONNECT_FROM_INCOGNITO
              : IDS_EXTENSION_PROMPT_EXTENSION_CONNECT_FROM_INCOGNITO;
      tab_context.infobar = IncognitoConnectabilityInfoBarDelegate::Create(
          infobar_manager, l10n_util::GetStringFUTF16(
                               template_id, base::UTF8ToUTF16(origin.spec()),
                               base::UTF8ToUTF16(extension->name())),
          base::Bind(&IncognitoConnectability::OnInteractiveResponse,
                     weak_factory_.GetWeakPtr(), extension->id(), origin,
                     infobar_manager));
      break;
    }

    // Testing code can override to always allow or deny.
    case ScopedAlertTracker::ALWAYS_ALLOW:
    case ScopedAlertTracker::ALWAYS_DENY:
      OnInteractiveResponse(extension->id(), origin, infobar_manager,
                            g_alert_mode);
      break;
  }
}

IncognitoConnectability::TabContext::TabContext() : infobar(nullptr) {
}

IncognitoConnectability::TabContext::TabContext(const TabContext& other) =
    default;

IncognitoConnectability::TabContext::~TabContext() {
}

void IncognitoConnectability::OnInteractiveResponse(
    const std::string& extension_id,
    const GURL& origin,
    InfoBarManager* infobar_manager,
    ScopedAlertTracker::Mode response) {
  switch (response) {
    case ScopedAlertTracker::ALWAYS_ALLOW:
      allowed_origins_[extension_id].insert(origin);
      break;
    case ScopedAlertTracker::ALWAYS_DENY:
      disallowed_origins_[extension_id].insert(origin);
      break;
    default:
      // Otherwise the user has not expressed an explicit preference and so
      // nothing should be permanently recorded.
      break;
  }

  DCHECK(ContainsKey(pending_origins_, make_pair(extension_id, origin)));
  PendingOrigin& pending_origin =
      pending_origins_[make_pair(extension_id, origin)];
  DCHECK(ContainsKey(pending_origin, infobar_manager));

  std::vector<base::Callback<void(bool)>> callbacks;
  if (response == ScopedAlertTracker::INTERACTIVE) {
    // No definitive answer for this extension and origin. Execute only the
    // callbacks associated with this tab.
    TabContext& tab_context = pending_origin[infobar_manager];
    callbacks.swap(tab_context.callbacks);
    pending_origin.erase(infobar_manager);
  } else {
    // We have a definitive answer for this extension and origin. Close all
    // other infobars and answer all the callbacks.
    for (const auto& map_entry : pending_origin) {
      InfoBarManager* other_infobar_manager = map_entry.first;
      const TabContext& other_tab_context = map_entry.second;
      if (other_infobar_manager != infobar_manager) {
        // Disarm the delegate so that it doesn't think the infobar has been
        // dismissed.
        IncognitoConnectabilityInfoBarDelegate* delegate =
            static_cast<IncognitoConnectabilityInfoBarDelegate*>(
                other_tab_context.infobar->delegate());
        delegate->set_answered();
        other_infobar_manager->RemoveInfoBar(other_tab_context.infobar);
      }
      callbacks.insert(callbacks.end(), other_tab_context.callbacks.begin(),
                       other_tab_context.callbacks.end());
    }
    pending_origins_.erase(make_pair(extension_id, origin));
  }

  DCHECK(!callbacks.empty());
  for (const auto& callback : callbacks) {
    callback.Run(response == ScopedAlertTracker::ALWAYS_ALLOW);
  }
}

bool IncognitoConnectability::IsInMap(const Extension* extension,
                                      const GURL& origin,
                                      const ExtensionToOriginsMap& map) {
  DCHECK_EQ(origin, origin.GetOrigin());
  ExtensionToOriginsMap::const_iterator it = map.find(extension->id());
  return it != map.end() && it->second.count(origin) > 0;
}

static base::LazyInstance<
    BrowserContextKeyedAPIFactory<IncognitoConnectability> > g_factory =
    LAZY_INSTANCE_INITIALIZER;

// static
BrowserContextKeyedAPIFactory<IncognitoConnectability>*
IncognitoConnectability::GetFactoryInstance() {
  return g_factory.Pointer();
}

}  // namespace extensions