// Copyright 2020 The Chromium Authors
// 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_service.h"

#include <string>
#include <unordered_set>

#include "base/containers/flat_map.h"
#include "base/metrics/histogram_functions.h"
#include "base/scoped_observation.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/component_updater/soda_language_pack_component_installer.h"
#include "chrome/grit/generated_resources.h"
#include "components/live_caption/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/soda/constants.h"
#include "components/soda/soda_installer.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/service_process_host.h"
#include "media/base/media_switches.h"
#include "media/mojo/mojom/speech_recognition_service.mojom.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_features.h"
#endif  // BUILDFLAG(IS_CHROMEOS)

namespace speech {

constexpr base::TimeDelta kIdleProcessTimeout = base::Seconds(5);

ChromeSpeechRecognitionService::ChromeSpeechRecognitionService(
    content::BrowserContext* context)
    : context_(context) {
#if BUILDFLAG(IS_CHROMEOS)
  if (!base::FeatureList::IsEnabled(
          ash::features::kOnDeviceSpeechRecognition)) {
    return;
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  auto* soda_installer = speech::SodaInstaller::GetInstance();

  // The SodaInstaller might not exist in unit tests.
  if (soda_installer) {
    soda_installer_observer_.Observe(soda_installer);
  }
}

ChromeSpeechRecognitionService::~ChromeSpeechRecognitionService() = default;

void ChromeSpeechRecognitionService::BindSpeechRecognitionContext(
    mojo::PendingReceiver<media::mojom::SpeechRecognitionContext> receiver) {
  LaunchIfNotRunning();

  if (speech_recognition_service_.is_bound()) {
    speech_recognition_service_->BindSpeechRecognitionContext(
        std::move(receiver));
  }
}

void ChromeSpeechRecognitionService::BindAudioSourceSpeechRecognitionContext(
    mojo::PendingReceiver<media::mojom::AudioSourceSpeechRecognitionContext>
        receiver) {
  LaunchIfNotRunning();

  if (speech_recognition_service_.is_bound()) {
    speech_recognition_service_->BindAudioSourceSpeechRecognitionContext(
        std::move(receiver));
  }
}

void ChromeSpeechRecognitionService::OnSodaInstalled(
    speech::LanguageCode language_code) {
  if (speech_recognition_service_.is_bound()) {
    speech_recognition_service_->SetSodaConfigPaths(
        ChromeSpeechRecognitionService::GetSodaConfigPaths());
  }
}

void ChromeSpeechRecognitionService::OnSodaInstallError(
    speech::LanguageCode language_code,
    speech::SodaInstaller::ErrorCode error_code) {}

void ChromeSpeechRecognitionService::OnSodaProgress(
    speech::LanguageCode language_code,
    int progress) {}

void ChromeSpeechRecognitionService::LaunchIfNotRunning() {
  if (speech_recognition_service_.is_bound()) {
    return;
  }

  PrefService* profile_prefs = user_prefs::UserPrefs::Get(context_);
  PrefService* global_prefs = g_browser_process->local_state();
  DCHECK(profile_prefs);
  DCHECK(global_prefs);

  // TODO(crbug.com/40162502): Language pack path should be configurable per
  // SpeechRecognitionRecognizer to allow multiple features to use Speech
  // recognition. For now, only Live Caption uses SpeechRecognitionService on
  // non-Chrome OS Chrome, so hard-coding to the Live Caption language code.
  const std::string language_name =
      prefs::GetLiveCaptionLanguageCode(profile_prefs);

  std::optional<speech::SodaLanguagePackComponentConfig> language_config =
      speech::GetLanguageComponentConfig(language_name);
  CHECK(language_config);
  base::UmaHistogramEnumeration("Accessibility.LiveCaption.SodaLanguage",
                                language_config.value().language_code);

  base::FilePath binary_path;
  binary_path = global_prefs->GetFilePath(prefs::kSodaBinaryPath);
  base::flat_map<std::string, base::FilePath> config_paths =
      ChromeSpeechRecognitionService::GetSodaConfigPaths();

  if (binary_path.empty() || config_paths[language_name].empty()) {
    LOG(ERROR) << "Unable to find SODA files on the device.";
    return;
  }

  content::ServiceProcessHost::Launch(
      speech_recognition_service_.BindNewPipeAndPassReceiver(),
      content::ServiceProcessHost::Options()
          .WithDisplayName(IDS_UTILITY_PROCESS_SPEECH_RECOGNITION_SERVICE_NAME)
          .Pass());

  // Ensure that if the interface is ever disconnected (e.g. the service
  // process crashes) or goes idle for a short period of time -- meaning there
  // are no in-flight messages and no other interfaces bound through this
  // one -- then we will reset |remote|, causing the service process to be
  // terminated if it isn't already.
  speech_recognition_service_.reset_on_disconnect();
  speech_recognition_service_.reset_on_idle_timeout(kIdleProcessTimeout);
  speech_recognition_service_->SetSodaPaths(binary_path, config_paths,
                                            language_name);

  bool mask_offensive_words =
      profile_prefs->GetBoolean(prefs::kLiveCaptionMaskOffensiveWords);
  speech_recognition_service_->SetSodaParams(mask_offensive_words);
}

base::flat_map<std::string, base::FilePath>
ChromeSpeechRecognitionService::GetSodaConfigPaths() {
  base::flat_map<std::string, base::FilePath> config_file_paths;
  std::unordered_set<std::string_view> registered_language_packs;
  for (const auto& language : g_browser_process->local_state()->GetList(
           prefs::kSodaRegisteredLanguagePacks)) {
    registered_language_packs.insert(language.GetString());
  }

  for (const SodaLanguagePackComponentConfig& config :
       kLanguageComponentConfigs) {
    base::FilePath config_path =
        g_browser_process->local_state()->GetFilePath(config.config_path_pref);

    if (!config_path.empty() &&
        registered_language_packs.contains(config.language_name)) {
      config_file_paths[config.language_name] = config_path;
    }
  }

  return config_file_paths;
}
}  // namespace speech
