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

#include <set>
#include <string>

#include "base/bind.h"
#include "base/synchronization/lock.h"
#include "base/threading/thread_restrictions.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/speech/chrome_speech_recognition_preferences.h"
#include "chrome/browser/speech/speech_recognition_tray_icon_controller.h"
#include "chrome/browser/tab_contents/tab_util.h"
#include "chrome/browser/view_type_utils.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_registrar.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/resource_context.h"
#include "content/public/browser/speech_recognition_manager.h"
#include "content/public/browser/speech_recognition_session_config.h"
#include "content/public/browser/speech_recognition_session_context.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/speech_recognition_error.h"
#include "content/public/common/speech_recognition_result.h"
#include "grit/generated_resources.h"
#include "net/url_request/url_request_context_getter.h"
#include "ui/base/l10n/l10n_util.h"

#if defined(OS_WIN)
#include "chrome/installer/util/wmi.h"
#endif

using content::BrowserThread;
using content::SpeechRecognitionManager;
using content::WebContents;

namespace {

const char kExtensionPrefix[] = "chrome-extension://";

bool RequiresBubble(int session_id) {
  return SpeechRecognitionManager::GetInstance()->
      GetSessionContext(session_id).requested_by_page_element;
}

bool RequiresTrayIcon(int session_id) {
  return !RequiresBubble(session_id);
}

}  // namespace

namespace speech {

// Asynchronously fetches the PC and audio hardware/driver info if
// the user has opted into UMA. This information is sent with speech input
// requests to the server for identifying and improving quality issues with
// specific device configurations.
class ChromeSpeechRecognitionManagerDelegate::OptionalRequestInfo
    : public base::RefCountedThreadSafe<OptionalRequestInfo> {
 public:
  OptionalRequestInfo() : can_report_metrics_(false) {
  }

  void Refresh() {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
    // UMA opt-in can be checked only from the UI thread, so switch to that.
    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
        base::Bind(&OptionalRequestInfo::CheckUMAAndGetHardwareInfo, this));
  }

  void CheckUMAAndGetHardwareInfo() {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    if (g_browser_process->local_state()->GetBoolean(
        prefs::kMetricsReportingEnabled)) {
      // Access potentially slow OS calls from the FILE thread.
      BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
          base::Bind(&OptionalRequestInfo::GetHardwareInfo, this));
    }
  }

  void GetHardwareInfo() {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
    base::AutoLock lock(lock_);
    can_report_metrics_ = true;
    string16 device_model =
        SpeechRecognitionManager::GetInstance()->GetAudioInputDeviceModel();
#if defined(OS_WIN)
    value_ = UTF16ToUTF8(
        installer::WMIComputerSystem::GetModel() + L"|" + device_model);
#else  // defined(OS_WIN)
    value_ = UTF16ToUTF8(device_model);
#endif  // defined(OS_WIN)
  }

  std::string value() {
    base::AutoLock lock(lock_);
    return value_;
  }

  bool can_report_metrics() {
    base::AutoLock lock(lock_);
    return can_report_metrics_;
  }

 private:
  friend class base::RefCountedThreadSafe<OptionalRequestInfo>;

  ~OptionalRequestInfo() {}

  base::Lock lock_;
  std::string value_;
  bool can_report_metrics_;

  DISALLOW_COPY_AND_ASSIGN(OptionalRequestInfo);
};

// Simple utility to get notified when a WebContent (a tab or an extension's
// background page) is closed or crashes. Both the callback site and the
// callback thread are passed by the caller in the constructor.
// There is no restriction on the constructor, however this class must be
// destroyed on the UI thread, due to the NotificationRegistrar dependency.
class ChromeSpeechRecognitionManagerDelegate::TabWatcher
    : public base::RefCountedThreadSafe<TabWatcher>,
      public content::NotificationObserver {
 public:
  typedef base::Callback<void(int render_process_id, int render_view_id)>
      TabClosedCallback;

  TabWatcher(TabClosedCallback tab_closed_callback,
             BrowserThread::ID callback_thread)
      : tab_closed_callback_(tab_closed_callback),
        callback_thread_(callback_thread) {
  }

  // Starts monitoring the WebContents corresponding to the given
  // |render_process_id|, |render_view_id| pair, invoking |tab_closed_callback_|
  // if closed/unloaded.
  void Watch(int render_process_id, int render_view_id) {
    if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
      BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(
          &TabWatcher::Watch, this, render_process_id, render_view_id));
      return;
    }

    WebContents* web_contents = tab_util::GetWebContentsByID(render_process_id,
                                                             render_view_id);
    // Sessions initiated by speech input extension APIs will end up in a NULL
    // WebContent here, but they are properly managed by the
    // chrome::SpeechInputExtensionManager. However, sessions initiated within a
    // extension using the (new) speech JS APIs, will be properly handled here.
    // TODO(primiano) turn this line into a DCHECK once speech input extension
    // API is deprecated.
    if (!web_contents)
      return;

    // Avoid multiple registrations on |registrar_| for the same |web_contents|.
    if (registered_web_contents_.find(web_contents) !=
        registered_web_contents_.end()) {
      return;
    }
    registered_web_contents_.insert(web_contents);

    // Lazy initialize the registrar.
    if (!registrar_.get())
      registrar_.reset(new content::NotificationRegistrar());

    registrar_->Add(this,
                    content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
                    content::Source<WebContents>(web_contents));
  }

  // content::NotificationObserver implementation.
  virtual void Observe(int type,
                       const content::NotificationSource& source,
                       const content::NotificationDetails& details) OVERRIDE {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    DCHECK_EQ(content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED, type);

    WebContents* web_contents = content::Source<WebContents>(source).ptr();
    int render_process_id = web_contents->GetRenderProcessHost()->GetID();
    int render_view_id = web_contents->GetRenderViewHost()->GetRoutingID();

    registrar_->Remove(this,
                       content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
                       content::Source<WebContents>(web_contents));
    registered_web_contents_.erase(web_contents);

    BrowserThread::PostTask(callback_thread_, FROM_HERE, base::Bind(
        tab_closed_callback_, render_process_id, render_view_id));
  }

 private:
  friend class base::RefCountedThreadSafe<TabWatcher>;

  virtual ~TabWatcher() {
    // Must be destroyed on the UI thread due to |registrar_| non thread-safety.
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  }

  // Lazy-initialized and used on the UI thread to handle web contents
  // notifications (tab closing).
  scoped_ptr<content::NotificationRegistrar> registrar_;

  // Keeps track of which WebContent(s) have been registered, in order to avoid
  // double registrations on |registrar_|
  std::set<content::WebContents*> registered_web_contents_;

  // Callback used to notify, on the thread specified by |callback_thread_| the
  // closure of a registered tab.
  TabClosedCallback tab_closed_callback_;
  content::BrowserThread::ID callback_thread_;

  DISALLOW_COPY_AND_ASSIGN(TabWatcher);
};

ChromeSpeechRecognitionManagerDelegate
::ChromeSpeechRecognitionManagerDelegate() {
}

ChromeSpeechRecognitionManagerDelegate
::~ChromeSpeechRecognitionManagerDelegate() {
  if (tray_icon_controller_.get())
    tray_icon_controller_->Hide();
  if (bubble_controller_.get())
    bubble_controller_->CloseBubble();
}

void ChromeSpeechRecognitionManagerDelegate::InfoBubbleButtonClicked(
    int session_id, SpeechRecognitionBubble::Button button) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));

  // Note, the session might have been destroyed, therefore avoid calls to the
  // manager which imply its existance (e.g., GetSessionContext()).

  if (button == SpeechRecognitionBubble::BUTTON_CANCEL) {
    GetBubbleController()->CloseBubble();
    last_session_config_.reset();

    // We can safely call AbortSession even if the session has already ended,
    // the manager's public methods are reliable and will handle it properly.
    SpeechRecognitionManager::GetInstance()->AbortSession(session_id);
  } else if (button == SpeechRecognitionBubble::BUTTON_TRY_AGAIN) {
    GetBubbleController()->CloseBubble();
    RestartLastSession();
  } else {
    NOTREACHED();
  }
}

void ChromeSpeechRecognitionManagerDelegate::InfoBubbleFocusChanged(
    int session_id) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));

  // This check is needed since on some systems (MacOS), in rare cases, if the
  // user clicks repeatedly and fast on the input element, the FocusChanged
  // event (corresponding to the old session that should be aborted) can be
  // received after a new session (corresponding to the 2nd click) is started.
  if (GetBubbleController()->GetActiveSessionID() != session_id)
    return;

  // Note, the session might have been destroyed, therefore avoid calls to the
  // manager which imply its existance (e.g., GetSessionContext()).
  GetBubbleController()->CloseBubble();
  last_session_config_.reset();

  // Clicking outside the bubble means we should abort.
  SpeechRecognitionManager::GetInstance()->AbortSession(session_id);
}

void ChromeSpeechRecognitionManagerDelegate::RestartLastSession() {
  DCHECK(last_session_config_.get());
  SpeechRecognitionManager* manager = SpeechRecognitionManager::GetInstance();
  const int new_session_id = manager->CreateSession(*last_session_config_);
  DCHECK_NE(SpeechRecognitionManager::kSessionIDInvalid, new_session_id);
  last_session_config_.reset();
  manager->StartSession(new_session_id);
}

void ChromeSpeechRecognitionManagerDelegate::TabClosedCallback(
    int render_process_id, int render_view_id) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));

  SpeechRecognitionManager* manager = SpeechRecognitionManager::GetInstance();
  // |manager| becomes NULL if a browser shutdown happens between the post of
  // this task (from the UI thread) and this call (on the IO thread). In this
  // case we just return.
  if (!manager)
    return;

  manager->AbortAllSessionsForRenderView(render_process_id, render_view_id);

  if (bubble_controller_.get() &&
      bubble_controller_->IsShowingBubbleForRenderView(render_process_id,
                                                       render_view_id)) {
    bubble_controller_->CloseBubble();
  }
}

void ChromeSpeechRecognitionManagerDelegate::OnRecognitionStart(
    int session_id) {
  const content::SpeechRecognitionSessionContext& context =
      SpeechRecognitionManager::GetInstance()->GetSessionContext(session_id);

  if (RequiresBubble(session_id)) {
    // Copy the configuration of the session (for the "try again" button).
    last_session_config_.reset(new content::SpeechRecognitionSessionConfig(
        SpeechRecognitionManager::GetInstance()->GetSessionConfig(session_id)));

    // Create and show the bubble.
    GetBubbleController()->CreateBubble(session_id,
                                        context.render_process_id,
                                        context.render_view_id,
                                        context.element_rect);
  }

  // Register callback to auto abort session on tab closure.
  // |tab_watcher_| is lazyly istantiated on the first call.
  if (!tab_watcher_.get()) {
    tab_watcher_ = new TabWatcher(
        base::Bind(&ChromeSpeechRecognitionManagerDelegate::TabClosedCallback,
                   base::Unretained(this)),
        BrowserThread::IO);
  }
  tab_watcher_->Watch(context.render_process_id, context.render_view_id);
}

void ChromeSpeechRecognitionManagerDelegate::OnAudioStart(int session_id) {
  if (RequiresBubble(session_id)) {
    DCHECK_EQ(session_id, GetBubbleController()->GetActiveSessionID());
    GetBubbleController()->SetBubbleRecordingMode();
  } else if (RequiresTrayIcon(session_id)) {
    // We post the action to the UI thread for sessions requiring a tray icon,
    // since ChromeSpeechRecognitionPreferences (which requires UI thread) is
    // involved for determining whether a security alert balloon is required.
    const content::SpeechRecognitionSessionContext& context =
        SpeechRecognitionManager::GetInstance()->GetSessionContext(session_id);
    BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(
        &ChromeSpeechRecognitionManagerDelegate::ShowTrayIconOnUIThread,
        context.context_name,
        context.render_process_id,
        scoped_refptr<SpeechRecognitionTrayIconController>(
            GetTrayIconController())));
  }
}

void ChromeSpeechRecognitionManagerDelegate::OnEnvironmentEstimationComplete(
    int session_id) {
}

void ChromeSpeechRecognitionManagerDelegate::OnSoundStart(int session_id) {
}

void ChromeSpeechRecognitionManagerDelegate::OnSoundEnd(int session_id) {
}

void ChromeSpeechRecognitionManagerDelegate::OnAudioEnd(int session_id) {
  // OnAudioEnd can be also raised after an abort, when the bubble has already
  // been closed.
  if (GetBubbleController()->GetActiveSessionID() == session_id) {
    DCHECK(RequiresBubble(session_id));
    GetBubbleController()->SetBubbleRecognizingMode();
  } else if (RequiresTrayIcon(session_id)) {
    GetTrayIconController()->Hide();
  }
}

void ChromeSpeechRecognitionManagerDelegate::OnRecognitionResult(
    int session_id, const content::SpeechRecognitionResult& result) {
  // The bubble will be closed upon the OnEnd event, which will follow soon.
}

void ChromeSpeechRecognitionManagerDelegate::OnRecognitionError(
    int session_id, const content::SpeechRecognitionError& error) {
  // An error can be dispatched when the bubble is not visible anymore.
  if (GetBubbleController()->GetActiveSessionID() != session_id)
    return;
  DCHECK(RequiresBubble(session_id));

  int error_message_id = 0;
  switch (error.code) {
    case content::SPEECH_RECOGNITION_ERROR_AUDIO:
      switch (error.details) {
        case content::SPEECH_AUDIO_ERROR_DETAILS_NO_MIC:
          error_message_id = IDS_SPEECH_INPUT_NO_MIC;
          break;
        case content::SPEECH_AUDIO_ERROR_DETAILS_IN_USE:
          error_message_id = IDS_SPEECH_INPUT_MIC_IN_USE;
          break;
        default:
          error_message_id = IDS_SPEECH_INPUT_MIC_ERROR;
          break;
      }
      break;
    case content::SPEECH_RECOGNITION_ERROR_ABORTED:
      error_message_id = IDS_SPEECH_INPUT_ABORTED;
      break;
    case content::SPEECH_RECOGNITION_ERROR_NO_SPEECH:
      error_message_id = IDS_SPEECH_INPUT_NO_SPEECH;
      break;
    case content::SPEECH_RECOGNITION_ERROR_NO_MATCH:
      error_message_id = IDS_SPEECH_INPUT_NO_RESULTS;
      break;
    case content::SPEECH_RECOGNITION_ERROR_NETWORK:
      error_message_id = IDS_SPEECH_INPUT_NET_ERROR;
      break;
    default:
      NOTREACHED() << "unknown error " << error.code;
      return;
  }
  GetBubbleController()->SetBubbleMessage(
      l10n_util::GetStringUTF16(error_message_id));
}

void ChromeSpeechRecognitionManagerDelegate::OnAudioLevelsChange(
    int session_id, float volume, float noise_volume) {
  if (GetBubbleController()->GetActiveSessionID() == session_id) {
    DCHECK(RequiresBubble(session_id));
    GetBubbleController()->SetBubbleInputVolume(volume, noise_volume);
  } else if (RequiresTrayIcon(session_id)) {
    GetTrayIconController()->SetVUMeterVolume(volume);
  }
}

void ChromeSpeechRecognitionManagerDelegate::OnRecognitionEnd(int session_id) {
  // The only case in which the OnRecognitionEnd should not close the bubble is
  // when we are showing an error. In this case the bubble will be closed by
  // the |InfoBubbleFocusChanged| method, when the users clicks either the
  // "Cancel" button or outside of the bubble.
  if (GetBubbleController()->GetActiveSessionID() == session_id &&
      !GetBubbleController()->IsShowingMessage()) {
    DCHECK(RequiresBubble(session_id));
    GetBubbleController()->CloseBubble();
  }
}

void ChromeSpeechRecognitionManagerDelegate::GetDiagnosticInformation(
    bool* can_report_metrics,
    std::string* hardware_info) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  if (!optional_request_info_.get()) {
    optional_request_info_ = new OptionalRequestInfo();
    // Since hardware info is optional with speech input requests, we start an
    // asynchronous fetch here and move on with recording audio. This first
    // speech input request would send an empty string for hardware info and
    // subsequent requests may have the hardware info available if the fetch
    // completed before them. This way we don't end up stalling the user with
    // a long wait and disk seeks when they click on a UI element and start
    // speaking.
    optional_request_info_->Refresh();
  }
  *can_report_metrics = optional_request_info_->can_report_metrics();
  *hardware_info = optional_request_info_->value();
}

void ChromeSpeechRecognitionManagerDelegate::CheckRecognitionIsAllowed(
    int session_id,
    base::Callback<void(bool ask_user, bool is_allowed)> callback) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));

  const content::SpeechRecognitionSessionContext& context =
      SpeechRecognitionManager::GetInstance()->GetSessionContext(session_id);

  // Make sure that initiators (extensions/web pages) properly set the
  // |render_process_id| field, which is needed later to retrieve the
  // ChromeSpeechRecognitionPreferences associated to their profile.
  DCHECK_NE(context.render_process_id, 0);

  // Check that the render view type is appropriate, and whether or not we
  // need to request permission from the user.
  BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
                          base::Bind(&CheckRenderViewType,
                                     callback,
                                     context.render_process_id,
                                     context.render_view_id,
                                     RequiresTrayIcon(session_id)));
}

content::SpeechRecognitionEventListener*
ChromeSpeechRecognitionManagerDelegate::GetEventListener() {
  return this;
}

void ChromeSpeechRecognitionManagerDelegate::ShowTrayIconOnUIThread(
    const std::string& context_name,
    int render_process_id,
    scoped_refptr<SpeechRecognitionTrayIconController> tray_icon_controller) {
  content::RenderProcessHost* render_process_host =
      content::RenderProcessHost::FromID(render_process_id);
  DCHECK(render_process_host);
  content::BrowserContext* browser_context =
      render_process_host->GetBrowserContext();
  Profile* profile = Profile::FromBrowserContext(browser_context);
  scoped_refptr<ChromeSpeechRecognitionPreferences> pref =
      ChromeSpeechRecognitionPreferences::GetForProfile(profile);
  bool show_notification = pref->ShouldShowSecurityNotification(context_name);
  if (show_notification)
    pref->SetHasShownSecurityNotification(context_name);

  // Speech recognitions initiated by JS APIs within an extension (so NOT by
  // extension API) will come with a context_name like "chrome-extension://id"
  // (that is, their origin as injected by WebKit). In such cases we try to
  // lookup the extension name, in order to show a more user-friendly balloon.
  string16 initiator_name = UTF8ToUTF16(context_name);
  if (context_name.find(kExtensionPrefix) == 0) {
    const std::string extension_id =
        context_name.substr(sizeof(kExtensionPrefix) - 1);
    const extensions::Extension* extension =
          profile->GetExtensionService()->GetExtensionById(extension_id, true);
    DCHECK(extension);
    initiator_name = UTF8ToUTF16(extension->name());
  }

  tray_icon_controller->Show(initiator_name, show_notification);
}

void ChromeSpeechRecognitionManagerDelegate::CheckRenderViewType(
    base::Callback<void(bool ask_user, bool is_allowed)> callback,
    int render_process_id,
    int render_view_id,
    bool js_api) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  const content::RenderViewHost* render_view_host =
      content::RenderViewHost::FromID(render_process_id, render_view_id);

  bool allowed = false;
  bool ask_permission = false;

  if (!render_view_host) {
    if (!js_api) {
      // If there is no render view, we cannot show the speech bubble, so this
      // is not allowed.
      allowed = false;
      ask_permission = false;
    } else {
      // This happens for extensions. Manifest should be checked for permission.
      allowed = true;
      ask_permission = false;
    }
    BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
                            base::Bind(callback, ask_permission, allowed));
    return;
  }

  WebContents* web_contents = WebContents::FromRenderViewHost(render_view_host);
  chrome::ViewType view_type = chrome::GetViewType(web_contents);

  if (view_type == chrome::VIEW_TYPE_TAB_CONTENTS) {
    // If it is a tab, we can show the speech input bubble or ask for
    // permission.

    allowed = true;
    if (js_api)
      ask_permission = true;
  }

  BrowserThread::PostTask(BrowserThread::IO, FROM_HERE,
                          base::Bind(callback, ask_permission, allowed));
}

SpeechRecognitionBubbleController*
ChromeSpeechRecognitionManagerDelegate::GetBubbleController() {
  if (!bubble_controller_.get())
    bubble_controller_ = new SpeechRecognitionBubbleController(this);
  return bubble_controller_.get();
}

SpeechRecognitionTrayIconController*
ChromeSpeechRecognitionManagerDelegate::GetTrayIconController() {
  if (!tray_icon_controller_.get())
    tray_icon_controller_ = new SpeechRecognitionTrayIconController();
  return tray_icon_controller_.get();
}


}  // namespace speech