/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

#include "CubebInputStream.h"

#include "AudioSampleFormat.h"
#include "mozilla/Logging.h"

namespace mozilla {

extern mozilla::LazyLogModule gMediaTrackGraphLog;

#ifdef LOG_INTERNAL
#  undef LOG_INTERNAL
#endif  // LOG_INTERNAL
#define LOG_INTERNAL(level, msg, ...) \
  MOZ_LOG_FMT(gMediaTrackGraphLog, LogLevel::level, msg, ##__VA_ARGS__)

#ifdef LOG
#  undef LOG
#endif  // LOG
#define LOG(msg, ...) LOG_INTERNAL(Debug, msg, ##__VA_ARGS__)

#ifdef LOGE
#  undef LOGE
#endif  // LOGE
#define LOGE(msg, ...) LOG_INTERNAL(Error, msg, ##__VA_ARGS__)

#define InvokeCubebWithLog(func, ...)                                    \
  ({                                                                     \
    int _retval;                                                         \
    _retval = InvokeCubeb(func, ##__VA_ARGS__);                          \
    if (_retval == CUBEB_OK) {                                           \
      LOG("CubebInputStream {}: {} for stream {} was successful",        \
          fmt::ptr(this), #func, fmt::ptr(mStream.get()));               \
    } else {                                                             \
      LOGE("CubebInputStream {}: {} for stream {} was failed. Error {}", \
           fmt::ptr(this), #func, fmt::ptr(mStream.get()), _retval);     \
    }                                                                    \
    _retval;                                                             \
  })

static cubeb_stream_params CreateStreamInitParams(uint32_t aChannels,
                                                  uint32_t aRate,
                                                  bool aIsVoice) {
  cubeb_stream_params params;
  params.format = CubebUtils::ToCubebFormat<AUDIO_OUTPUT_FORMAT>::value;
  params.rate = aRate;
  params.channels = aChannels;
  params.layout = CUBEB_LAYOUT_UNDEFINED;
  params.prefs = CubebUtils::GetDefaultStreamPrefs(CUBEB_DEVICE_TYPE_INPUT);
  params.input_params = CUBEB_INPUT_PROCESSING_PARAM_NONE;

  if (aIsVoice) {
    params.prefs |= static_cast<cubeb_stream_prefs>(CUBEB_STREAM_PREF_VOICE);
  }

  return params;
}

void CubebInputStream::CubebDestroyPolicy::operator()(
    cubeb_stream* aStream) const {
  int r = cubeb_stream_register_device_changed_callback(aStream, nullptr);
  if (r == CUBEB_OK) {
    LOG("Unregister device changed callback for {} successfully",
        fmt::ptr(aStream));
  } else {
    LOGE("Fail to unregister device changed callback for {}. Error {}",
         fmt::ptr(aStream), r);
  }
  cubeb_stream_destroy(aStream);
}

/* static */
UniquePtr<CubebInputStream> CubebInputStream::Create(cubeb_devid aDeviceId,
                                                     uint32_t aChannels,
                                                     uint32_t aRate,
                                                     bool aIsVoice,
                                                     Listener* aListener) {
  if (!aListener) {
    LOGE("No available listener");
    return nullptr;
  }

  RefPtr<CubebUtils::CubebHandle> handle = CubebUtils::GetCubeb();
  if (!handle) {
    LOGE("No valid cubeb context");
    CubebUtils::ReportCubebStreamInitFailure(CubebUtils::GetFirstStream());
    return nullptr;
  }

  cubeb_stream_params params =
      CreateStreamInitParams(aChannels, aRate, aIsVoice);
  uint32_t latencyFrames = CubebUtils::GetCubebMTGLatencyInFrames(&params);

  cubeb_stream* cubebStream = nullptr;

  RefPtr<Listener> listener(aListener);
  if (int r = CubebUtils::CubebStreamInit(
          handle->Context(), &cubebStream, "input-only stream", aDeviceId,
          &params, nullptr, nullptr, latencyFrames, DataCallback_s,
          StateCallback_s, listener.get());
      r != CUBEB_OK) {
    CubebUtils::ReportCubebStreamInitFailure(CubebUtils::GetFirstStream());
    LOGE("Fail to create a cubeb stream. Error {}", r);
    return nullptr;
  }

  UniquePtr<cubeb_stream, CubebDestroyPolicy> inputStream(cubebStream);

  LOG("Create a cubeb stream {} successfully", fmt::ptr(inputStream.get()));

  UniquePtr<CubebInputStream> stream(new CubebInputStream(
      listener.forget(), handle.forget(), std::move(inputStream)));
  stream->Init();
  return stream;
}

CubebInputStream::CubebInputStream(
    already_AddRefed<Listener> aListener,
    already_AddRefed<CubebUtils::CubebHandle> aCubeb,
    UniquePtr<cubeb_stream, CubebDestroyPolicy>&& aStream)
    : mListener(aListener), mCubeb(aCubeb), mStream(std::move(aStream)) {
  MOZ_ASSERT(mListener);
  MOZ_ASSERT(mStream);
}

void CubebInputStream::Init() {
  // cubeb_stream_register_device_changed_callback is only supported on macOS
  // platform and MockCubeb for now.
  InvokeCubebWithLog(cubeb_stream_register_device_changed_callback,
                     CubebInputStream::DeviceChangedCallback_s);
}

int CubebInputStream::Start() { return InvokeCubebWithLog(cubeb_stream_start); }

int CubebInputStream::Stop() { return InvokeCubebWithLog(cubeb_stream_stop); }

int CubebInputStream::SetProcessingParams(
    cubeb_input_processing_params aParams) {
  return InvokeCubebWithLog(cubeb_stream_set_input_processing_params, aParams);
}

int CubebInputStream::Latency(uint32_t* aLatencyFrames) {
  return InvokeCubebWithLog(cubeb_stream_get_input_latency, aLatencyFrames);
}

template <typename Function, typename... Args>
int CubebInputStream::InvokeCubeb(Function aFunction, Args&&... aArgs) {
  MOZ_ASSERT(mStream);
  return aFunction(mStream.get(), std::forward<Args>(aArgs)...);
}

/* static */
long CubebInputStream::DataCallback_s(cubeb_stream* aStream, void* aUser,
                                      const void* aInputBuffer,
                                      void* aOutputBuffer, long aFrames) {
  MOZ_ASSERT(aUser);
  MOZ_ASSERT(aInputBuffer);
  MOZ_ASSERT(!aOutputBuffer);
  return static_cast<Listener*>(aUser)->DataCallback(aInputBuffer, aFrames);
}

/* static */
void CubebInputStream::StateCallback_s(cubeb_stream* aStream, void* aUser,
                                       cubeb_state aState) {
  MOZ_ASSERT(aUser);
  static_cast<Listener*>(aUser)->StateCallback(aState);
}

/* static */
void CubebInputStream::DeviceChangedCallback_s(void* aUser) {
  MOZ_ASSERT(aUser);
  static_cast<Listener*>(aUser)->DeviceChangedCallback();
}

#undef LOG_INTERNAL
#undef LOG
#undef LOGE

}  // namespace mozilla
