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

#include <string>

#include "base/containers/flat_map.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/memory/weak_ptr.h"
#include "chrome/services/speech/audio_source_fetcher_impl.h"
#include "chrome/services/speech/soda_speech_recognizer_impl.h"
#include "chrome/services/speech/speech_recognition_recognizer_impl.h"
#include "media/mojo/mojom/speech_recognition.mojom.h"
#include "media/mojo/mojom/speech_recognition_service.mojom.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"

namespace speech {

namespace {

constexpr char kInvalidSpeechRecogntionOptions[] =
    "Invalid SpeechRecognitionOptions provided";

}  // namespace

SpeechRecognitionServiceImpl::SpeechRecognitionServiceImpl(
    mojo::PendingReceiver<media::mojom::SpeechRecognitionService> receiver)
    : receiver_(this, std::move(receiver)) {}

SpeechRecognitionServiceImpl::~SpeechRecognitionServiceImpl() = default;

void SpeechRecognitionServiceImpl::BindAudioSourceSpeechRecognitionContext(
    mojo::PendingReceiver<media::mojom::AudioSourceSpeechRecognitionContext>
        context) {
  // TODO(b/249867435): Make this function create mojo::ReportBadMessage because
  // this method shouldn't be called on platforms other than ChromeOS. ChromeOS
  // clients should use CrosSpeechRecognitionService.
  audio_source_speech_recognition_contexts_.Add(this, std::move(context));
}

void SpeechRecognitionServiceImpl::BindSpeechRecognitionContext(
    mojo::PendingReceiver<media::mojom::SpeechRecognitionContext> context) {
  speech_recognition_contexts_.Add(this, std::move(context));
}

void SpeechRecognitionServiceImpl::SetSodaPaths(
    const base::FilePath& binary_path,
    const base::flat_map<std::string, base::FilePath>& config_paths,
    const std::string& default_live_caption_language) {
  binary_path_ = binary_path;
  config_paths_ = config_paths;
  default_live_caption_language_ = default_live_caption_language;
}

void SpeechRecognitionServiceImpl::SetSodaParams(
    const bool mask_offensive_words) {
  mask_offensive_words_ = mask_offensive_words;
}

void SpeechRecognitionServiceImpl::SetSodaConfigPaths(
    const base::flat_map<std::string, base::FilePath>& config_paths) {
  config_paths_ = config_paths;
  for (Observer& observer : observers_) {
    observer.OnLanguagePackInstalled(config_paths_);
  }
}

void SpeechRecognitionServiceImpl::BindRecognizer(
    mojo::PendingReceiver<media::mojom::SpeechRecognitionRecognizer> receiver,
    mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizerClient> client,
    media::mojom::SpeechRecognitionOptionsPtr options,
    BindRecognizerCallback callback) {
  if (CreateRecognizer(std::move(receiver), std::move(client),
                       std::move(options))) {
    std::move(callback).Run(
        SpeechRecognitionRecognizerImpl::IsMultichannelSupported());
  }
}

void SpeechRecognitionServiceImpl::BindWebSpeechRecognizer(
    mojo::PendingReceiver<media::mojom::SpeechRecognitionSession>
        session_receiver,
    mojo::PendingRemote<media::mojom::SpeechRecognitionSessionClient>
        session_client,
    mojo::PendingReceiver<media::mojom::SpeechRecognitionAudioForwarder>
        audio_forwarder,
    int channel_count,
    int sample_rate,
    media::mojom::SpeechRecognitionOptionsPtr options,
    bool continuous) {
  mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizer>
      speech_recognition_recognizer;
  mojo::PendingReceiver<media::mojom::SpeechRecognitionRecognizerClient>
      speech_recognition_recognizer_client;

  if (CreateRecognizer(
          speech_recognition_recognizer.InitWithNewPipeAndPassReceiver(),
          speech_recognition_recognizer_client.InitWithNewPipeAndPassRemote(),
          std::move(options))) {
    mojo::MakeSelfOwnedReceiver(
        std::make_unique<SodaSpeechRecognizerImpl>(
            continuous, sample_rate, std::move(speech_recognition_recognizer),
            std::move(speech_recognition_recognizer_client),
            std::move(session_client), std::move(audio_forwarder)),
        std::move(session_receiver));
  }
}

void SpeechRecognitionServiceImpl::BindAudioSourceFetcher(
    mojo::PendingReceiver<media::mojom::AudioSourceFetcher> fetcher_receiver,
    mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizerClient> client,
    media::mojom::SpeechRecognitionOptionsPtr options,
    BindRecognizerCallback callback) {
  const bool is_server_based = options->is_server_based;
  // TODO(b/249867435): Remove SpeechRecognitionServiceImpl's implementation of
  // AudioSourceSpeechRecognitionContext should be removed. AudioSourceFetcher
  // is only ever used from ChromeOS and it should be accessed
  // from CrosSpeechRecognitionService.
  if (is_server_based) {
    mojo::ReportBadMessage(kInvalidSpeechRecogntionOptions);
    return;
  }

  // Destroy the speech recognition service if the SODA files haven't been
  // downloaded yet.
  if (!FilePathsExist()) {
    speech_recognition_contexts_.Clear();
    receiver_.reset();
    return;
  }

  auto recognizer = std::make_unique<SpeechRecognitionRecognizerImpl>(
      std::move(client), std::move(options), binary_path_, config_paths_,
      default_live_caption_language_, mask_offensive_words_,
      weak_factory_.GetWeakPtr());

// On Chrome OS, CrosSpeechRecognitionRecognizerImpl will create its own
// CrosSodaClient.
#if !BUILDFLAG(IS_CHROMEOS)
  recognizer->CreateSodaClient(binary_path_);
#endif

  const bool is_multi_channel_supported =
      SpeechRecognitionRecognizerImpl::IsMultichannelSupported();
  AudioSourceFetcherImpl::Create(std::move(fetcher_receiver),
                                 std::move(recognizer),
                                 is_multi_channel_supported, is_server_based);
  std::move(callback).Run(is_multi_channel_supported);
}

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

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

bool SpeechRecognitionServiceImpl::FilePathsExist() {
  if (!base::PathExists(binary_path_))
    return false;

  for (const auto& config : config_paths_) {
    if (!base::PathExists(config.second))
      return false;
  }

  return true;
}

bool SpeechRecognitionServiceImpl::CreateRecognizer(
    mojo::PendingReceiver<media::mojom::SpeechRecognitionRecognizer> receiver,
    mojo::PendingRemote<media::mojom::SpeechRecognitionRecognizerClient> client,
    media::mojom::SpeechRecognitionOptionsPtr options) {
  // This is currently only used by Live Caption and server side
  // speech recognition is not available to it.
  if (options->is_server_based ||
      options->recognizer_client_type !=
          media::mojom::RecognizerClientType::kLiveCaption) {
    mojo::ReportBadMessage(kInvalidSpeechRecogntionOptions);
    return false;
  }

  // Destroy the speech recognition service if the SODA files haven't been
  // downloaded yet.
  if (!FilePathsExist()) {
    speech_recognition_contexts_.Clear();
    receiver_.reset();
    return false;
  }

  auto language = options->language.has_value()
                      ? options->language.value()
                      : default_live_caption_language_;
  SpeechRecognitionRecognizerImpl::Create(
      std::move(receiver), std::move(client), std::move(options), binary_path_,
      config_paths_, language, mask_offensive_words_,
      weak_factory_.GetWeakPtr());

  return true;
}

}  // namespace speech
