// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/audio/fuchsia/audio_input_stream_fuchsia.h"

#include <lib/sys/cpp/component_context.h>
#include <lib/zx/vmo.h>

#include "base/bits.h"
#include "base/fuchsia/fuchsia_logging.h"
#include "base/fuchsia/process_context.h"
#include "base/logging.h"
#include "base/notimplemented.h"
#include "media/audio/audio_device_description.h"
#include "media/audio/fuchsia/audio_manager_fuchsia.h"
#include "media/base/audio_bus.h"
#include "media/base/audio_sample_types.h"

namespace media {

namespace {

// Currently AudioCapturer supports only one payload buffer with id=0.
constexpr uint32_t kBufferId = 0;

// Number of audio packets that should fit in the capture buffer.
constexpr size_t kBufferPacketCapacity = 10;

bool IsDeviceStateMuted(const fuchsia::settings::DeviceState& state) {
  return state.has_toggle_flags() &&
         (state.toggle_flags() & fuchsia::settings::ToggleStateFlags::MUTED) ==
             fuchsia::settings::ToggleStateFlags::MUTED;
}

}  // namespace

AudioInputStreamFuchsia::AudioInputStreamFuchsia(
    AudioManagerFuchsia* manager,
    const AudioParameters& parameters,
    std::string device_id)
    : manager_(manager),
      parameters_(parameters),
      device_id_(std::move(device_id)) {
  DCHECK(device_id_.empty() ||
         device_id_ == AudioDeviceDescription::kLoopbackInputDeviceId ||
         device_id_ == AudioDeviceDescription::kDefaultDeviceId)
      << "AudioInput from " << device_id_ << " not implemented!";
  DCHECK(parameters_.format() == AudioParameters::AUDIO_PCM_LOW_LATENCY ||
         parameters_.format() == AudioParameters::AUDIO_PCM_LINEAR);
}

AudioInputStreamFuchsia::~AudioInputStreamFuchsia() = default;

AudioInputStream::OpenOutcome AudioInputStreamFuchsia::Open() {
  // Open() can be called only once.
  DCHECK(!capturer_);

  auto factory = base::ComponentContextForProcess()
                     ->svc()
                     ->Connect<fuchsia::media::Audio>();
  bool is_loopback =
      device_id_ == AudioDeviceDescription::kLoopbackInputDeviceId;
  factory->CreateAudioCapturer(capturer_.NewRequest(), is_loopback);
  capturer_.set_error_handler([this](zx_status_t status) {
    ZX_LOG(ERROR, status) << "AudioCapturer disconnected";
    ReportError(Error::kRuntimeError);
  });

  // Bind the event for incoming packets.
  capturer_.events().OnPacketProduced =
      fit::bind_member(this, &AudioInputStreamFuchsia::OnPacketProduced);

  // Configure stream format.
  fuchsia::media::AudioStreamType stream_type;
  stream_type.sample_format = fuchsia::media::AudioSampleFormat::FLOAT;
  stream_type.channels = parameters_.channels();
  stream_type.frames_per_second = parameters_.sample_rate();
  capturer_->SetPcmStreamType(std::move(stream_type));

  // Allocate shared buffer.
  const size_t page_size = static_cast<size_t>(zx_system_get_page_size());
  size_t capture_buffer_size =
      parameters_.GetBytesPerBuffer(kSampleFormatF32) * kBufferPacketCapacity;
  capture_buffer_size = base::bits::AlignUp(capture_buffer_size, page_size);

  zx::vmo buffer_vmo;
  zx_status_t status = zx::vmo::create(capture_buffer_size, 0, &buffer_vmo);
  ZX_CHECK(status == ZX_OK, status) << "zx_vmo_create";

  constexpr char kName[] = "cr-audio-capturer";
  status = buffer_vmo.set_property(ZX_PROP_NAME, kName, std::size(kName) - 1);
  ZX_DCHECK(status == ZX_OK, status);

  bool mapped =
      capture_buffer_.Initialize(std::move(buffer_vmo), /*writable=*/false,
                                 /*offset=*/0, /*size=*/capture_buffer_size,
                                 fuchsia::sysmem2::CoherencyDomain::CPU);

  if (!mapped)
    return OpenOutcome::kFailed;

  // Pass the buffer to the capturer.
  capturer_->AddPayloadBuffer(kBufferId,
                              capture_buffer_.Duplicate(/*writable=*/true));

  if (!is_loopback) {
    input_service_ = base::ComponentContextForProcess()
                         ->svc()
                         ->Connect<fuchsia::settings::Input>();
    input_service_.set_error_handler([](zx_status_t status) {
      ZX_LOG(ERROR, status) << "fuchsia.settings.Input disconnected";
    });
    WatchInputSettings();
  }

  return OpenOutcome::kSuccess;
}

void AudioInputStreamFuchsia::Start(AudioInputCallback* callback) {
  if (!capturer_) {
    callback->OnError(Error::kStartupFailed);
    return;
  }

  callback_ = callback;

  if (!is_capturer_started_) {
    is_capturer_started_ = true;
    capturer_->StartAsyncCapture(parameters_.frames_per_buffer());
  }
}

void AudioInputStreamFuchsia::Stop() {
  // Normally Close() is called immediately after Stop(), so there is no need to
  // stop the capturer. Just release the |callback_| to ensure it's not called
  // again.
  callback_ = nullptr;
}

void AudioInputStreamFuchsia::Close() {
  Stop();
  if (manager_)
    manager_->ReleaseInputStream(this);
}

double AudioInputStreamFuchsia::GetMaxVolume() {
  return 1.0;
}

void AudioInputStreamFuchsia::SetVolume(double volume) {
  NOTIMPLEMENTED();
}

double AudioInputStreamFuchsia::GetVolume() {
  return 1.0;
}

bool AudioInputStreamFuchsia::SetAutomaticGainControl(bool enabled) {
  NOTIMPLEMENTED();
  return false;
}

bool AudioInputStreamFuchsia::GetAutomaticGainControl() {
  return false;
}

bool AudioInputStreamFuchsia::IsMuted() {
  return is_muted_;
}

void AudioInputStreamFuchsia::WatchInputSettings() {
  input_service_->Watch([this](fuchsia::settings::InputSettings settings) {
    OnInputSettingsReceived(std::move(settings));
  });
}

void AudioInputStreamFuchsia::OnInputSettingsReceived(
    fuchsia::settings::InputSettings settings) {
  bool mic_found = false;
  if (settings.has_devices()) {
    for (const auto& device : settings.devices()) {
      if (device.has_device_type() &&
          device.device_type() == fuchsia::settings::DeviceType::MICROPHONE) {
        // TODO(crbug.com/42050621): Match device_name when we support
        // selecting non-default devices.
        mic_found = true;
        bool is_muted = false;
        if (device.has_state()) {
          is_muted = IsDeviceStateMuted(device.state());
        }
        if (!is_muted && device.has_source_states()) {
          for (const auto& source : device.source_states()) {
            if (source.has_state() && IsDeviceStateMuted(source.state())) {
              is_muted = true;
              break;
            }
          }
        }
        is_muted_ = is_muted;
        break;
      }
    }
  }
  if (!mic_found) {
    LOG(WARNING) << "No microphone found in input settings.";
  }
  WatchInputSettings();
}

void AudioInputStreamFuchsia::SetOutputDeviceForAec(
    const std::string& output_device_id) {
  NOTIMPLEMENTED();
}

void AudioInputStreamFuchsia::OnPacketProduced(
    fuchsia::media::StreamPacket packet) {
  size_t bytes_per_frame = parameters_.GetBytesPerFrame(kSampleFormatF32);

  if (packet.payload_buffer_id != kBufferId ||
      packet.payload_offset + packet.payload_size > capture_buffer_.size() ||
      packet.payload_size % bytes_per_frame != 0 ||
      packet.payload_size < bytes_per_frame) {
    LOG(ERROR) << "Received invalid packet from AudioCapturer.";
    ReportError(Error::kRuntimeError);
    return;
  }

  if (callback_) {
    const int num_frames = packet.payload_size / bytes_per_frame;
    if (!audio_bus_ || num_frames != audio_bus_->frames()) {
      audio_bus_ = AudioBus::Create(parameters_.channels(), num_frames);
    }
    auto source = capture_buffer_.GetMemory().subspan(packet.payload_offset,
                                                      packet.payload_size);

    audio_bus_->FromInterleavedBytes<Float32SampleTypeTraits>(source);
    callback_->OnData(audio_bus_.get(), base::TimeTicks::FromZxTime(packet.pts),
                      /*volume=*/1.0, {});
  }

  capturer_->ReleasePacket(std::move(packet));
}

void AudioInputStreamFuchsia::ReportError(Error error_code) {
  capturer_.Unbind();
  if (callback_)
    callback_->OnError(error_code);
}

}  // namespace media
