// 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 "third_party/blink/renderer/modules/webcodecs/audio_decoder.h"

#include <memory>
#include <vector>

#include "base/metrics/histogram_functions.h"
#include "base/types/to_address.h"
#include "media/base/audio_codecs.h"
#include "media/base/audio_decoder.h"
#include "media/base/audio_decoder_config.h"
#include "media/base/channel_layout.h"
#include "media/base/encryption_scheme.h"
#include "media/base/media_util.h"
#include "media/base/mime_util.h"
#include "media/base/supported_types.h"
#include "media/base/waiting.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_decoder_config.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_decoder_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_decoder_support.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_encoded_audio_chunk.h"
#include "third_party/blink/renderer/modules/webaudio/base_audio_context.h"
#include "third_party/blink/renderer/modules/webcodecs/array_buffer_util.h"
#include "third_party/blink/renderer/modules/webcodecs/audio_data.h"
#include "third_party/blink/renderer/modules/webcodecs/audio_decoder_broker.h"
#include "third_party/blink/renderer/modules/webcodecs/decrypt_config_util.h"
#include "third_party/blink/renderer/modules/webcodecs/encoded_audio_chunk.h"
#include "third_party/blink/renderer/platform/audio/audio_utilities.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/wtf/text/format.h"

namespace blink {

bool VerifyDescription(const AudioDecoderConfig& config,
                       String* js_error_message) {
  // https://www.w3.org/TR/webcodecs-flac-codec-registration
  // https://www.w3.org/TR/webcodecs-vorbis-codec-registration
  bool description_required = false;
  if (config.codec() == "flac" || config.codec() == "vorbis") {
    description_required = true;
  }

  if (description_required && !config.hasDescription()) {
    *js_error_message = "Invalid config; description is required.";
    return false;
  }

  // For Opus with more than 2 channels, we need a description. While we can
  // guess a channel mapping for up to 8 channels, we don't know whether the
  // encoded Opus streams will be mono or stereo streams.
  if (config.codec() == "opus" && config.numberOfChannels() > 2 &&
      !config.hasDescription()) {
    *js_error_message =
        "Invalid config; description is required for multi-channel Opus.";
    return false;
  }

  if (config.hasDescription()) {
    auto desc_wrapper = AsSpan<const uint8_t>(config.description());

    if (!desc_wrapper.data()) {
      *js_error_message = "Invalid config; description is detached.";
      return false;
    }
  }

  return true;
}

AudioDecoderConfig* CopyConfig(const AudioDecoderConfig& config) {
  AudioDecoderConfig* copy = AudioDecoderConfig::Create();
  copy->setCodec(config.codec());
  copy->setSampleRate(config.sampleRate());
  copy->setNumberOfChannels(config.numberOfChannels());
  if (config.hasDescription()) {
    auto desc_wrapper = AsSpan<const uint8_t>(config.description());
    if (!desc_wrapper.empty()) {
      DOMArrayBuffer* buffer_copy = DOMArrayBuffer::Create(desc_wrapper);
      copy->setDescription(
          MakeGarbageCollected<AllowSharedBufferSource>(buffer_copy));
    }
  }
  return copy;
}

std::optional<media::AudioCodec> TryGetPcmCodec(const String& codec) {
  String codecs_str = codec.ToAsciiLower();
  if (codecs_str == "ulaw") {
    return media::AudioCodec::kPCM_MULAW;
  }

  if (codecs_str == "alaw") {
    return media::AudioCodec::kPCM_ALAW;
  }

  if (codecs_str == "pcm-u8" || codecs_str == "pcm-s16" ||
      codecs_str == "pcm-s24" || codecs_str == "pcm-s32" ||
      codecs_str == "pcm-f32") {
    return media::AudioCodec::kPCM;
  }

  return std::nullopt;
}

media::SampleFormat PcmCodecToSampleFormat(const String& codec) {
  String codecs_str = codec.ToAsciiLower();

  if (codecs_str == "pcm-u8") {
    return media::SampleFormat::kSampleFormatU8;
  }

  if (codecs_str == "pcm-s16") {
    return media::SampleFormat::kSampleFormatS16;
  }

  if (codecs_str == "pcm-s24") {
    return media::SampleFormat::kSampleFormatS24;
  }

  if (codecs_str == "pcm-s32") {
    return media::SampleFormat::kSampleFormatS32;
  }

  if (codecs_str == "pcm-f32") {
    return media::SampleFormat::kSampleFormatF32;
  }

  return media::SampleFormat::kSampleFormatPlanarF32;
}

// static
std::unique_ptr<AudioDecoderTraits::MediaDecoderType>
AudioDecoderTraits::CreateDecoder(
    ExecutionContext& execution_context,
    media::GpuVideoAcceleratorFactories* gpu_factories,
    media::MediaLog* media_log) {
  return std::make_unique<AudioDecoderBroker>(media_log, execution_context);
}

// static
void AudioDecoderTraits::UpdateDecoderLog(const MediaDecoderType& decoder,
                                          const MediaConfigType& media_config,
                                          media::MediaLog* media_log) {
  media_log->SetProperty<media::MediaLogProperty::kAudioDecoderName>(
      decoder.GetDecoderType());
  media_log->SetProperty<media::MediaLogProperty::kIsPlatformAudioDecoder>(
      decoder.IsPlatformDecoder());
  media_log->SetProperty<media::MediaLogProperty::kAudioTracks>(
      std::vector<MediaConfigType>{media_config});
  MEDIA_LOG(INFO, media_log)
      << "Initialized AudioDecoder: " << media_config.AsHumanReadableString();
  base::UmaHistogramEnumeration("Blink.WebCodecs.AudioDecoder.Codec",
                                media_config.codec());
}

// static
void AudioDecoderTraits::InitializeDecoder(
    MediaDecoderType& decoder,
    bool /*low_delay*/,
    const MediaConfigType& media_config,
    MediaDecoderType::InitCB init_cb,
    MediaDecoderType::OutputCB output_cb) {
  decoder.Initialize(media_config, nullptr /* cdm_context */,
                     std::move(init_cb), output_cb, media::WaitingCB());
}

// static
int AudioDecoderTraits::GetMaxDecodeRequests(const MediaDecoderType& decoder) {
  return 1;
}

// static
const char* AudioDecoderTraits::GetName() {
  return "AudioDecoder";
}

// static
AudioDecoder* AudioDecoder::Create(ScriptState* script_state,
                                   const AudioDecoderInit* init,
                                   ExceptionState& exception_state) {
  auto* result =
      MakeGarbageCollected<AudioDecoder>(script_state, init, exception_state);
  return exception_state.HadException() ? nullptr : result;
}

// static
ScriptPromise<AudioDecoderSupport> AudioDecoder::isConfigSupported(
    ScriptState* script_state,
    const AudioDecoderConfig* config,
    ExceptionState& exception_state) {
  String js_error_message;
  std::optional<media::AudioType> audio_type =
      IsValidAudioDecoderConfig(*config, &js_error_message);

  if (!audio_type) {
    exception_state.ThrowTypeError(js_error_message);
    return EmptyPromise();
  }

  AudioDecoderSupport* support = AudioDecoderSupport::Create();
  support->setSupported(media::IsDecoderSupportedAudioType(*audio_type));
  support->setConfig(CopyConfig(*config));
  return ToResolvedPromise<AudioDecoderSupport>(script_state, support);
}

// static
std::optional<media::AudioType> AudioDecoder::IsValidAudioDecoderConfig(
    const AudioDecoderConfig& config,
    String* js_error_message) {
  if (config.numberOfChannels() == 0) {
    *js_error_message = Format(
        "Invalid channel count; channel count must be non-zero, received {}.",
        config.numberOfChannels());
    return std::nullopt;
  }

  if (config.sampleRate() == 0) {
    *js_error_message = Format(
        "Invalid sample rate; sample rate must be non-zero, received {}.",
        config.sampleRate());
    return std::nullopt;
  }

  if (config.codec().LengthWithStrippedWhiteSpace() == 0) {
    *js_error_message = "Invalid codec; codec is required.";
    return std::nullopt;
  }
  // Match codec strings from the codec registry:
  // https://www.w3.org/TR/webcodecs-codec-registry/#audio-codec-registry
  std::optional<media::AudioCodec> pcm_type = TryGetPcmCodec(config.codec());
  if (pcm_type.has_value()) {
    return media::AudioType{.codec = *pcm_type};
  }

  if (!VerifyDescription(config, js_error_message)) {
    CHECK(!js_error_message->empty());
    return std::nullopt;
  }

  std::optional<media::AudioType> audio_type =
      media::ParseAudioCodecString("", config.codec().Utf8());
  if (!audio_type) {
    *js_error_message = "Unknown or ambiguous codec name.";
    return media::AudioType{.codec = media::AudioCodec::kUnknown};
  }

  return audio_type;
}

// static
std::optional<media::AudioDecoderConfig>
AudioDecoder::MakeMediaAudioDecoderConfig(const ConfigType& config,
                                          String* js_error_message) {
  std::optional<media::AudioType> audio_type =
      IsValidAudioDecoderConfig(config, js_error_message);
  if (!audio_type) {
    // Checked by IsValidConfig().
    NOTREACHED();
  }
  if (audio_type->codec == media::AudioCodec::kUnknown) {
    return std::nullopt;
  }

  std::vector<uint8_t> extra_data;
  if (config.hasDescription()) {
    auto desc_wrapper = AsSpan<const uint8_t>(config.description());

    if (!desc_wrapper.data()) {
      // We should never get here, since this should be caught in
      // IsValidAudioDecoderConfig().
      *js_error_message = "Invalid config; description is detached.";
      return std::nullopt;
    }

    if (config.codec() == "opus") {
      // A size of 19 bytes corresponds to the minimum length of an Opus
      // Identification Header for a standard mono or stereo stream.
      constexpr size_t kMinDescriptionSize = 19;
      if (desc_wrapper.size() < kMinDescriptionSize) {
        *js_error_message = "Invalid config; description is too short.";
        return std::nullopt;
      }
    }

    if (!desc_wrapper.empty()) {
      extra_data.assign(base::to_address(desc_wrapper.begin()),
                        base::to_address(desc_wrapper.end()));
    }
  }

  media::ChannelLayoutConfig channel_layout =
      media::ChannelLayoutConfig::Guess(config.numberOfChannels());

  auto encryption_scheme = media::EncryptionScheme::kUnencrypted;
  if (config.hasEncryptionScheme()) {
    auto scheme = ToMediaEncryptionScheme(config.encryptionScheme());
    if (!scheme) {
      *js_error_message = "Unsupported encryption scheme";
      return std::nullopt;
    }
    encryption_scheme = scheme.value();
  }

  // TODO(chcunningham): Add sample format to IDL.
  media::AudioDecoderConfig media_config;

  media::SampleFormat format = media::kSampleFormatPlanarF32;
  if (audio_type->codec == media::AudioCodec::kPCM) {
    // There is a case of the codec being "1", which is a valid PCM codec for
    // WAV in media/base/mime_util_internal.cc. We should reject this case for
    // webcodecs.
    if (config.codec() == "1") {
      return std::nullopt;
    }
    format = PcmCodecToSampleFormat(config.codec());

    // Both FFmpeg and Symphonia exclusively output S16 decoded buffers for ALAW
    // and MULAW.
  } else if (audio_type->codec == media::AudioCodec::kPCM_ALAW ||
             audio_type->codec == media::AudioCodec::kPCM_MULAW) {
    format = media::SampleFormat::kSampleFormatS16;
  }

  media_config.Initialize(audio_type->codec, format, channel_layout,
                          config.sampleRate(), extra_data, encryption_scheme,
                          /*seek_preroll=*/base::TimeDelta(),
                          /*codec_delay=*/0);
  if (!media_config.IsValidConfig()) {
    *js_error_message = "Unsupported config.";
    return std::nullopt;
  }

  return media_config;
}

AudioDecoder::AudioDecoder(ScriptState* script_state,
                           const AudioDecoderInit* init,
                           ExceptionState& exception_state)
    : DecoderTemplate<AudioDecoderTraits>(script_state, init, exception_state) {
  UseCounter::Count(ExecutionContext::From(script_state),
                    WebFeature::kWebCodecs);
}

bool AudioDecoder::IsValidConfig(const ConfigType& config,
                                 String* js_error_message) {
  return IsValidAudioDecoderConfig(config, js_error_message /* out */)
      .has_value();
}

std::optional<media::AudioDecoderConfig> AudioDecoder::MakeMediaConfig(
    const ConfigType& config,
    String* js_error_message) {
  DCHECK(js_error_message);
  return MakeMediaAudioDecoderConfig(config, js_error_message /* out */);
}

media::DecoderStatus::Or<scoped_refptr<media::DecoderBuffer>>
AudioDecoder::MakeInput(const InputType& chunk, bool verify_key_frame) {
  if (verify_key_frame && !chunk.buffer()->is_key_frame())
    return media::DecoderStatus::Codes::kKeyFrameRequired;
  return chunk.buffer();
}

media::DecoderStatus::Or<AudioDecoder::OutputType*> AudioDecoder::MakeOutput(
    scoped_refptr<MediaOutputType> output,
    ExecutionContext* context) {
  if (!blink::audio_utilities::IsValidAudioBufferSampleRate(
          output->sample_rate())) {
    return media::DecoderStatus(
        media::DecoderStatus::Codes::kInvalidArgument,
        Format("Invalid decoded audio output sample rate. Got {}, which is "
               "outside [{:f}, {:f}]",
               output->sample_rate(),
               blink::audio_utilities::MinAudioBufferSampleRate(),
               blink::audio_utilities::MaxAudioBufferSampleRate())
            .Ascii());
  }

  if (static_cast<uint32_t>(output->channel_count()) >
      BaseAudioContext::MaxNumberOfChannels()) {
    return media::DecoderStatus(
        media::DecoderStatus::Codes::kInvalidArgument,
        Format("Invalid decoded audio output channel count. Got {}, which "
               "exceeds {}",
               output->channel_count(), BaseAudioContext::MaxNumberOfChannels())
            .Ascii());
  }

  return MakeGarbageCollected<AudioDecoderTraits::OutputType>(
      std::move(output));
}

const AtomicString& AudioDecoder::InterfaceName() const {
  return event_target_names::kAudioDecoder;
}

}  // namespace blink
