/* 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 http://mozilla.org/MPL/2.0/. */

#include "MockCubeb.h"

#include <thread>

namespace mozilla {

using KeepProcessing = MockCubebStream::KeepProcessing;

void PrintDevice(cubeb_device_info aInfo) {
  printf(
      "id: %zu\n"
      "device_id: %s\n"
      "friendly_name: %s\n"
      "group_id: %s\n"
      "vendor_name: %s\n"
      "type: %d\n"
      "state: %d\n"
      "preferred: %d\n"
      "format: %d\n"
      "default_format: %d\n"
      "max_channels: %d\n"
      "default_rate: %d\n"
      "max_rate: %d\n"
      "min_rate: %d\n"
      "latency_lo: %d\n"
      "latency_hi: %d\n",
      reinterpret_cast<uintptr_t>(aInfo.devid), aInfo.device_id,
      aInfo.friendly_name, aInfo.group_id, aInfo.vendor_name, aInfo.type,
      aInfo.state, aInfo.preferred, aInfo.format, aInfo.default_format,
      aInfo.max_channels, aInfo.default_rate, aInfo.max_rate, aInfo.min_rate,
      aInfo.latency_lo, aInfo.latency_hi);
}

void PrintDevice(AudioDeviceInfo* aInfo) {
  cubeb_devid id;
  nsString name;
  nsString groupid;
  nsString vendor;
  uint16_t type;
  uint16_t state;
  uint16_t preferred;
  uint16_t supportedFormat;
  uint16_t defaultFormat;
  uint32_t maxChannels;
  uint32_t defaultRate;
  uint32_t maxRate;
  uint32_t minRate;
  uint32_t maxLatency;
  uint32_t minLatency;

  id = aInfo->DeviceID();
  aInfo->GetName(name);
  aInfo->GetGroupId(groupid);
  aInfo->GetVendor(vendor);
  aInfo->GetType(&type);
  aInfo->GetState(&state);
  aInfo->GetPreferred(&preferred);
  aInfo->GetSupportedFormat(&supportedFormat);
  aInfo->GetDefaultFormat(&defaultFormat);
  aInfo->GetMaxChannels(&maxChannels);
  aInfo->GetDefaultRate(&defaultRate);
  aInfo->GetMaxRate(&maxRate);
  aInfo->GetMinRate(&minRate);
  aInfo->GetMinLatency(&minLatency);
  aInfo->GetMaxLatency(&maxLatency);

  printf(
      "device id: %zu\n"
      "friendly_name: %s\n"
      "group_id: %s\n"
      "vendor_name: %s\n"
      "type: %d\n"
      "state: %d\n"
      "preferred: %d\n"
      "format: %d\n"
      "default_format: %d\n"
      "max_channels: %d\n"
      "default_rate: %d\n"
      "max_rate: %d\n"
      "min_rate: %d\n"
      "latency_lo: %d\n"
      "latency_hi: %d\n",
      reinterpret_cast<uintptr_t>(id), NS_LossyConvertUTF16toASCII(name).get(),
      NS_LossyConvertUTF16toASCII(groupid).get(),
      NS_LossyConvertUTF16toASCII(vendor).get(), type, state, preferred,
      supportedFormat, defaultFormat, maxChannels, defaultRate, maxRate,
      minRate, minLatency, maxLatency);
}

cubeb_device_info DeviceTemplate(cubeb_devid aId, cubeb_device_type aType,
                                 const char* name) {
  // A fake input device
  cubeb_device_info device;
  device.devid = aId;
  device.device_id = "nice name";
  device.friendly_name = name;
  device.group_id = "the physical device";
  device.vendor_name = "mozilla";
  device.type = aType;
  device.state = CUBEB_DEVICE_STATE_ENABLED;
  device.preferred = CUBEB_DEVICE_PREF_NONE;
  device.format = CUBEB_DEVICE_FMT_F32NE;
  device.default_format = CUBEB_DEVICE_FMT_F32NE;
  device.max_channels = 2;
  device.default_rate = 44100;
  device.max_rate = 44100;
  device.min_rate = 16000;
  device.latency_lo = 256;
  device.latency_hi = 1024;

  return device;
}

cubeb_device_info DeviceTemplate(cubeb_devid aId, cubeb_device_type aType) {
  return DeviceTemplate(aId, aType, "nice name");
}

void AddDevices(MockCubeb* mock, uint32_t device_count,
                cubeb_device_type deviceType) {
  mock->ClearDevices(deviceType);
  // Add a few input devices (almost all the same but it does not really
  // matter as long as they have distinct IDs and only one is the default
  // devices)
  for (uintptr_t i = 0; i < device_count; i++) {
    cubeb_device_info device =
        DeviceTemplate(reinterpret_cast<void*>(i + 1), deviceType);
    // Make it so that the last device is the default input device.
    if (i == device_count - 1) {
      device.preferred = CUBEB_DEVICE_PREF_ALL;
    }
    mock->AddDevice(device);
  }
}

void cubeb_mock_destroy(cubeb* context) {
  MockCubeb::AsMock(context)->Destroy();
}

MockCubebStream::MockCubebStream(
    cubeb* aContext, char const* aStreamName, cubeb_devid aInputDevice,
    cubeb_stream_params* aInputStreamParams, cubeb_devid aOutputDevice,
    cubeb_stream_params* aOutputStreamParams, cubeb_data_callback aDataCallback,
    cubeb_state_callback aStateCallback, void* aUserPtr,
    SmartMockCubebStream* aSelf, RunningMode aRunningMode, bool aFrozenStart)
    : context(aContext),
      mUserPtr(aUserPtr),
      mRunningMode(aRunningMode),
      mHasInput(aInputStreamParams),
      mHasOutput(aOutputStreamParams),
      mSelf(aSelf),
      mFrozenStartMonitor("MockCubebStream::mFrozenStartMonitor"),
      mFrozenStart(aFrozenStart),
      mDataCallback(aDataCallback),
      mStateCallback(aStateCallback),
      mName(aStreamName),
      mInputDeviceID(aInputDevice),
      mOutputDeviceID(aOutputDevice),
      mAudioGenerator(aInputStreamParams ? aInputStreamParams->channels
                                         : MAX_INPUT_CHANNELS,
                      aInputStreamParams ? aInputStreamParams->rate
                                         : aOutputStreamParams->rate,
                      100 /* aFrequency */),
      mAudioVerifier(aInputStreamParams ? aInputStreamParams->rate
                                        : aOutputStreamParams->rate,
                     100 /* aFrequency */) {
  MOZ_RELEASE_ASSERT(mAudioGenerator.ChannelCount() <= MAX_INPUT_CHANNELS,
                     "mInputBuffer has no enough space to hold generated data");
  if (mFrozenStart) {
    MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  }
  if (aInputStreamParams) {
    mInputParams = *aInputStreamParams;
  }
  if (aOutputStreamParams) {
    mOutputParams = *aOutputStreamParams;
    MOZ_RELEASE_ASSERT(SampleRate() == mOutputParams.rate);
  }
}

MockCubebStream::~MockCubebStream() = default;

int MockCubebStream::Start() {
  {
    MutexAutoLock l(mMutex);
    NotifyState(CUBEB_STATE_STARTED);
  }
  {
    MonitorAutoLock lock(mFrozenStartMonitor);
    if (mFrozenStart) {
      // We need to grab mFrozenStartMonitor before returning to avoid races in
      // the calling code -- it controls when to mFrozenStartMonitor.Notify().
      // TempData helps facilitate this by holding what's needed to block the
      // calling thread until the background thread has grabbed the lock.
      struct TempData {
        NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TempData)
        static_assert(HasThreadSafeRefCnt::value,
                      "Silence a -Wunused-local-typedef warning");
        Monitor mMonitor{"MockCubebStream::Start::TempData::mMonitor"};
        bool mFinished = false;

       private:
        ~TempData() = default;
      };
      auto temp = MakeRefPtr<TempData>();
      MonitorAutoLock lock(temp->mMonitor);
      NS_DispatchBackgroundTask(NS_NewRunnableFunction(
          "MockCubebStream::WaitForThawBeforeStart",
          [temp, this, self = RefPtr<SmartMockCubebStream>(mSelf)]() mutable {
            {
              // Unblock MockCubebStream::Start now that we have locked the
              // frozen start monitor.
              MonitorAutoLock tempLock(temp->mMonitor);
              temp->mFinished = true;
              temp->mMonitor.Notify();
              temp = nullptr;
            }
            {
              MonitorAutoLock lock(mFrozenStartMonitor);
              while (mFrozenStart) {
                mFrozenStartMonitor.Wait();
              }
            }
            if (MutexAutoLock l(mMutex);
                !mState || *mState != CUBEB_STATE_STARTED) {
              return;
            }
            MockCubeb::AsMock(context)->StartStream(mSelf);
          }));
      while (!temp->mFinished) {
        temp->mMonitor.Wait();
      }
      return CUBEB_OK;
    }
  }
  MockCubeb::AsMock(context)->StartStream(this);
  return CUBEB_OK;
}

int MockCubebStream::Stop() {
  MockCubeb::AsMock(context)->StopStream(this);
  MutexAutoLock l(mMutex);
  mOutputVerificationEvent.Notify(std::make_tuple(
      mAudioVerifier.PreSilenceSamples(), mAudioVerifier.EstimatedFreq(),
      mAudioVerifier.CountDiscontinuities()));
  NotifyState(CUBEB_STATE_STOPPED);
  return CUBEB_OK;
}

uint64_t MockCubebStream::Position() {
  MutexAutoLock l(mMutex);
  return mPosition;
}

void MockCubebStream::Destroy() {
  // Stop() even if cubeb_stream_stop() has already been called, as with
  // audioipc.  https://bugzilla.mozilla.org/show_bug.cgi?id=1801190#c1
  // This provides an extra STOPPED state callback as with audioipc.
  // It also ensures that this stream is removed from MockCubeb::mLiveStreams.
  Stop();
  {
    MutexAutoLock l(mMutex);
    mDestroyed = true;
  }
  MockCubeb::AsMock(context)->StreamDestroy(this);
}

int MockCubebStream::SetName(char const* aName) {
  MutexAutoLock l(mMutex);
  mName = aName;
  mNameSetEvent.Notify(mName);
  return CUBEB_OK;
}

int MockCubebStream::RegisterDeviceChangedCallback(
    cubeb_device_changed_callback aDeviceChangedCallback) {
  MutexAutoLock l(mMutex);
  if (mDeviceChangedCallback && aDeviceChangedCallback) {
    return CUBEB_ERROR_INVALID_PARAMETER;
  }
  mDeviceChangedCallback = aDeviceChangedCallback;
  return CUBEB_OK;
}

int MockCubebStream::SetInputProcessingParams(
    cubeb_input_processing_params aParams) {
  MockCubeb* mock = MockCubeb::AsMock(context);
  auto res = mock->SupportedInputProcessingParams();
  if (res.isErr()) {
    return CUBEB_ERROR_NOT_SUPPORTED;
  }
  cubeb_input_processing_params supported = res.unwrap();
  if ((supported & aParams) != aParams) {
    return CUBEB_ERROR_INVALID_PARAMETER;
  }
  return mock->InputProcessingApplyRv();
}

cubeb_stream* MockCubebStream::AsCubebStream() {
  MutexAutoLock l(mMutex);
  return AsCubebStreamLocked();
}

cubeb_stream* MockCubebStream::AsCubebStreamLocked() {
  MOZ_RELEASE_ASSERT(!mDestroyed);
  mMutex.AssertCurrentThreadOwns();
  return reinterpret_cast<cubeb_stream*>(this);
}

MockCubebStream* MockCubebStream::AsMock(cubeb_stream* aStream) {
  auto* mockStream = reinterpret_cast<MockCubebStream*>(aStream);
  MutexAutoLock l(mockStream->mMutex);
  return AsMockLocked(aStream);
}

MockCubebStream* MockCubebStream::AsMockLocked(cubeb_stream* aStream) {
  auto* mockStream = reinterpret_cast<MockCubebStream*>(aStream);
  mockStream->mMutex.AssertCurrentThreadOwns();
  MOZ_RELEASE_ASSERT(!mockStream->mDestroyed);
  return mockStream;
}

cubeb_devid MockCubebStream::GetInputDeviceID() const { return mInputDeviceID; }

cubeb_devid MockCubebStream::GetOutputDeviceID() const {
  return mOutputDeviceID;
}

uint32_t MockCubebStream::InputChannels() const {
  MutexAutoLock l(mMutex);
  return InputChannelsLocked();
}

uint32_t MockCubebStream::InputChannelsLocked() const {
  mMutex.AssertCurrentThreadOwns();
  return mAudioGenerator.ChannelCount();
}

uint32_t MockCubebStream::OutputChannels() const {
  MutexAutoLock l(mMutex);
  return OutputChannelsLocked();
}

uint32_t MockCubebStream::OutputChannelsLocked() const {
  mMutex.AssertCurrentThreadOwns();
  return mOutputParams.channels;
}

uint32_t MockCubebStream::SampleRate() const {
  MutexAutoLock l(mMutex);
  return SampleRateLocked();
}

uint32_t MockCubebStream::SampleRateLocked() const {
  mMutex.AssertCurrentThreadOwns();
  return mAudioGenerator.mSampleRate;
}

uint32_t MockCubebStream::InputFrequency() const {
  MutexAutoLock l(mMutex);
  return InputFrequencyLocked();
}

uint32_t MockCubebStream::InputFrequencyLocked() const {
  mMutex.AssertCurrentThreadOwns();
  return mAudioGenerator.mFrequency;
}

Maybe<cubeb_state> MockCubebStream::State() const {
  MutexAutoLock l(mMutex);
  return mState;
}

nsTArray<AudioDataValue>&& MockCubebStream::TakeRecordedOutput() {
  MutexAutoLock l(mMutex);
  return std::move(mRecordedOutput);
}

nsTArray<AudioDataValue>&& MockCubebStream::TakeRecordedInput() {
  MutexAutoLock l(mMutex);
  return std::move(mRecordedInput);
}

void MockCubebStream::SetDriftFactor(float aDriftFactor) {
  MOZ_RELEASE_ASSERT(mRunningMode == MockCubeb::RunningMode::Automatic);
  MutexAutoLock l(mMutex);
  mDriftFactor = aDriftFactor;
}

void MockCubebStream::ForceError() {
  MutexAutoLock l(mMutex);
  mForceErrorState = true;
}

void MockCubebStream::ForceDeviceChanged() {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  MutexAutoLock l(mMutex);
  mForceDeviceChanged = true;
};

void MockCubebStream::NotifyDeviceChangedNow() {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Manual);
  NotifyDeviceChanged();
}

void MockCubebStream::Thaw() {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  MonitorAutoLock l(mFrozenStartMonitor);
  mFrozenStart = false;
  mFrozenStartMonitor.Notify();
}

void MockCubebStream::SetOutputRecordingEnabled(bool aEnabled) {
  MutexAutoLock l(mMutex);
  mOutputRecordingEnabled = aEnabled;
}

void MockCubebStream::SetInputRecordingEnabled(bool aEnabled) {
  MutexAutoLock l(mMutex);
  mInputRecordingEnabled = aEnabled;
}

MediaEventSource<nsCString>& MockCubebStream::NameSetEvent() {
  return mNameSetEvent;
}

MediaEventSource<cubeb_state>& MockCubebStream::StateEvent() {
  return mStateEvent;
}

MediaEventSource<uint32_t>& MockCubebStream::FramesProcessedEvent() {
  return mFramesProcessedEvent;
}

MediaEventSource<uint32_t>& MockCubebStream::FramesVerifiedEvent() {
  return mFramesVerifiedEvent;
}

MediaEventSource<std::tuple<uint64_t, float, uint32_t>>&
MockCubebStream::OutputVerificationEvent() {
  return mOutputVerificationEvent;
}

MediaEventSource<void>& MockCubebStream::ErrorForcedEvent() {
  return mErrorForcedEvent;
}

MediaEventSource<void>& MockCubebStream::DeviceChangeForcedEvent() {
  return mDeviceChangedForcedEvent;
}

KeepProcessing MockCubebStream::ManualDataCallback(long aNrFrames) {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Manual);
  MOZ_RELEASE_ASSERT(aNrFrames <= kMaxNrFrames);
  MutexAutoLock l(mMutex);
  return Process(aNrFrames);
}

KeepProcessing MockCubebStream::Process(long aNrFrames) {
  mMutex.AssertCurrentThreadOwns();
  if (!mState || *mState != CUBEB_STATE_STARTED) {
    return KeepProcessing::InvalidState;
  }
  if (mInputParams.rate) {
    mAudioGenerator.GenerateInterleaved(mInputBuffer, aNrFrames);
  }
  cubeb_stream* stream = AsCubebStreamLocked();
  const long outframes =
      mDataCallback(stream, mUserPtr, mHasInput ? mInputBuffer : nullptr,
                    mHasOutput ? mOutputBuffer : nullptr, aNrFrames);

  if (outframes > 0) {
    if (mInputRecordingEnabled && mHasInput) {
      mRecordedInput.AppendElements(mInputBuffer,
                                    outframes * InputChannelsLocked());
    }
    if (mOutputRecordingEnabled && mHasOutput) {
      mRecordedOutput.AppendElements(mOutputBuffer,

                                     outframes * OutputChannelsLocked());
    }
    mAudioVerifier.AppendDataInterleaved(mOutputBuffer, outframes,
                                         MAX_OUTPUT_CHANNELS);
    mPosition += outframes;

    mFramesProcessedEvent.Notify(outframes);
    if (mAudioVerifier.PreSilenceEnded()) {
      mFramesVerifiedEvent.Notify(outframes);
    }
  }

  if (outframes < aNrFrames) {
    NotifyState(CUBEB_STATE_DRAINED);
    return KeepProcessing::No;
  }
  if (mForceErrorState) {
    mForceErrorState = false;
    NotifyState(CUBEB_STATE_ERROR);
    mErrorForcedEvent.Notify();
    return KeepProcessing::No;
  }
  if (mForceDeviceChanged) {
    mForceDeviceChanged = false;
    // The device-changed callback is not necessary to be run in the
    // audio-callback thread. It's up to the platform APIs. We don't have any
    // control over them. Fire the device-changed callback in another thread to
    // simulate this.
    NS_DispatchBackgroundTask(NS_NewRunnableFunction(
        __func__, [this, self = RefPtr(mSelf)] { NotifyDeviceChanged(); }));
  }
  return KeepProcessing::Yes;
}

KeepProcessing MockCubebStream::Process10Ms() {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  MutexAutoLock l(mMutex);
  uint32_t rate = SampleRateLocked();
  const long nrFrames =
      static_cast<long>(static_cast<float>(rate * 10) * mDriftFactor) /
      PR_MSEC_PER_SEC;
  return Process(nrFrames);
}

void MockCubebStream::NotifyState(cubeb_state aState) {
  mMutex.AssertCurrentThreadOwns();
  mState = Some(aState);
  mStateCallback(AsCubebStreamLocked(), mUserPtr, aState);
  mStateEvent.Notify(aState);
}

void MockCubebStream::NotifyDeviceChanged() {
  MutexAutoLock l(mMutex);
  mDeviceChangedCallback(this->mUserPtr);
  mDeviceChangedForcedEvent.Notify();
}

MockCubeb::MockCubeb() : MockCubeb(MockCubeb::RunningMode::Automatic) {}

MockCubeb::MockCubeb(RunningMode aRunningMode)
    : ops(&mock_ops), mRunningMode(aRunningMode) {
  // Silence a -Wunused-private-field warning in clang.
  // Note [[maybe_unused]] could silence this but then gcc warns about
  // error: 'unused' attribute ignored [-Werror=attributes].
  (void)ops;
}

MockCubeb::~MockCubeb() { MOZ_RELEASE_ASSERT(!mFakeAudioThreadRunning); }

void MockCubeb::Destroy() {
  MOZ_RELEASE_ASSERT(mHasCubebContext);
  {
    auto streams = mLiveStreams.Lock();
    MOZ_RELEASE_ASSERT(streams->IsEmpty());
  }
  mDestroyed = true;
  Release();
}

cubeb* MockCubeb::AsCubebContext() {
  MOZ_RELEASE_ASSERT(!mDestroyed);
  if (mHasCubebContext.compareExchange(false, true)) {
    AddRef();
  }
  return reinterpret_cast<cubeb*>(this);
}

MockCubeb* MockCubeb::AsMock(cubeb* aContext) {
  auto* mockCubeb = reinterpret_cast<MockCubeb*>(aContext);
  MOZ_RELEASE_ASSERT(!mockCubeb->mDestroyed);
  return mockCubeb;
}

int MockCubeb::EnumerateDevices(cubeb_device_type aType,
                                cubeb_device_collection* aCollection) {
#ifdef ANDROID
  MOZ_ASSERT_UNREACHABLE("This is not to be called on Android.");
#endif
  size_t count = 0;
  if (aType & CUBEB_DEVICE_TYPE_INPUT) {
    count += mInputDevices.Length();
  }
  if (aType & CUBEB_DEVICE_TYPE_OUTPUT) {
    count += mOutputDevices.Length();
  }
  aCollection->device = new cubeb_device_info[count];
  aCollection->count = count;

  uint32_t collection_index = 0;
  if (aType & CUBEB_DEVICE_TYPE_INPUT) {
    for (auto& device : mInputDevices) {
      aCollection->device[collection_index] = device;
      collection_index++;
    }
  }
  if (aType & CUBEB_DEVICE_TYPE_OUTPUT) {
    for (auto& device : mOutputDevices) {
      aCollection->device[collection_index] = device;
      collection_index++;
    }
  }

  return CUBEB_OK;
}

int MockCubeb::DestroyDeviceCollection(cubeb_device_collection* aCollection) {
  delete[] aCollection->device;
  aCollection->count = 0;
  return CUBEB_OK;
}

int MockCubeb::RegisterDeviceCollectionChangeCallback(
    cubeb_device_type aDevType,
    cubeb_device_collection_changed_callback aCallback, void* aUserPtr) {
  if (!mSupportsDeviceCollectionChangedCallback) {
    return CUBEB_ERROR;
  }

  if (aDevType & CUBEB_DEVICE_TYPE_INPUT) {
    mInputDeviceCollectionChangeCallback = aCallback;
    mInputDeviceCollectionChangeUserPtr = aUserPtr;
  }
  if (aDevType & CUBEB_DEVICE_TYPE_OUTPUT) {
    mOutputDeviceCollectionChangeCallback = aCallback;
    mOutputDeviceCollectionChangeUserPtr = aUserPtr;
  }

  return CUBEB_OK;
}

Result<cubeb_input_processing_params, int>
MockCubeb::SupportedInputProcessingParams() const {
  const auto& [params, rv] = mSupportedInputProcessingParams;
  if (rv != CUBEB_OK) {
    return Err(rv);
  }
  return params;
}

void MockCubeb::SetSupportedInputProcessingParams(
    cubeb_input_processing_params aParams, int aRv) {
  mSupportedInputProcessingParams = std::make_pair(aParams, aRv);
}

void MockCubeb::SetInputProcessingApplyRv(int aRv) {
  mInputProcessingParamsApplyRv = aRv;
}

int MockCubeb::InputProcessingApplyRv() const {
  return mInputProcessingParamsApplyRv;
}

void MockCubeb::AddDevice(cubeb_device_info aDevice) {
  if (aDevice.type == CUBEB_DEVICE_TYPE_INPUT) {
    mInputDevices.AppendElement(aDevice);
  } else if (aDevice.type == CUBEB_DEVICE_TYPE_OUTPUT) {
    mOutputDevices.AppendElement(aDevice);
  } else {
    MOZ_CRASH("bad device type when adding a device in mock cubeb backend");
  }

  bool isInput = aDevice.type & CUBEB_DEVICE_TYPE_INPUT;
  if (isInput && mInputDeviceCollectionChangeCallback) {
    mInputDeviceCollectionChangeCallback(AsCubebContext(),
                                         mInputDeviceCollectionChangeUserPtr);
  }
  if (!isInput && mOutputDeviceCollectionChangeCallback) {
    mOutputDeviceCollectionChangeCallback(AsCubebContext(),
                                          mOutputDeviceCollectionChangeUserPtr);
  }
}

bool MockCubeb::RemoveDevice(cubeb_devid aId) {
  bool foundInput = false;
  bool foundOutput = false;
  mInputDevices.RemoveElementsBy(
      [aId, &foundInput](cubeb_device_info& aDeviceInfo) {
        bool foundThisTime = aDeviceInfo.devid == aId;
        foundInput |= foundThisTime;
        return foundThisTime;
      });
  mOutputDevices.RemoveElementsBy(
      [aId, &foundOutput](cubeb_device_info& aDeviceInfo) {
        bool foundThisTime = aDeviceInfo.devid == aId;
        foundOutput |= foundThisTime;
        return foundThisTime;
      });

  if (foundInput && mInputDeviceCollectionChangeCallback) {
    mInputDeviceCollectionChangeCallback(AsCubebContext(),
                                         mInputDeviceCollectionChangeUserPtr);
  }
  if (foundOutput && mOutputDeviceCollectionChangeCallback) {
    mOutputDeviceCollectionChangeCallback(AsCubebContext(),
                                          mOutputDeviceCollectionChangeUserPtr);
  }
  // If the device removed was a default device, set another device as the
  // default, if there are still devices available.
  bool foundDefault = false;
  for (uint32_t i = 0; i < mInputDevices.Length(); i++) {
    foundDefault |= mInputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE;
  }

  if (!foundDefault) {
    if (!mInputDevices.IsEmpty()) {
      mInputDevices[mInputDevices.Length() - 1].preferred =
          CUBEB_DEVICE_PREF_ALL;
    }
  }

  foundDefault = false;
  for (uint32_t i = 0; i < mOutputDevices.Length(); i++) {
    foundDefault |= mOutputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE;
  }

  if (!foundDefault) {
    if (!mOutputDevices.IsEmpty()) {
      mOutputDevices[mOutputDevices.Length() - 1].preferred =
          CUBEB_DEVICE_PREF_ALL;
    }
  }

  return foundInput | foundOutput;
}

void MockCubeb::ClearDevices(cubeb_device_type aType) {
  if (aType != CUBEB_DEVICE_TYPE_OUTPUT) {
    mInputDevices.Clear();
  }
  if (aType != CUBEB_DEVICE_TYPE_INPUT) {
    mOutputDevices.Clear();
  }
}

void MockCubeb::SetPreferredDevice(cubeb_devid aId, cubeb_device_type aType) {
  nsTArray<cubeb_device_info>& devs =
      aType == CUBEB_DEVICE_TYPE_INPUT ? mInputDevices : mOutputDevices;
  bool found{};
  for (const auto& dev : devs) {
    if (dev.devid == aId) {
      found = true;
      break;
    }
  }
  if (!found) {
    return;
  }
  for (auto& dev : devs) {
    dev.preferred =
        dev.devid == aId ? CUBEB_DEVICE_PREF_ALL : CUBEB_DEVICE_PREF_NONE;
  }
  auto& callback = aType == CUBEB_DEVICE_TYPE_INPUT
                       ? mInputDeviceCollectionChangeCallback
                       : mOutputDeviceCollectionChangeCallback;
  void* user_ptr = aType == CUBEB_DEVICE_TYPE_INPUT
                       ? mInputDeviceCollectionChangeUserPtr
                       : mOutputDeviceCollectionChangeUserPtr;
  if (found && callback) {
    callback(AsCubebContext(), user_ptr);
  }
}

void MockCubeb::SetSupportDeviceChangeCallback(bool aSupports) {
  mSupportsDeviceCollectionChangedCallback = aSupports;
}

void MockCubeb::ForceStreamInitError() { mStreamInitErrorState = true; }

void MockCubeb::SetStreamStartFreezeEnabled(bool aEnabled) {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  mStreamStartFreezeEnabled = aEnabled;
}

auto MockCubeb::ForceAudioThread() -> RefPtr<ForcedAudioThreadPromise> {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  RefPtr<ForcedAudioThreadPromise> p =
      mForcedAudioThreadPromise.Ensure(__func__);
  mForcedAudioThread = true;
  StartStream(nullptr);
  return p;
}

void MockCubeb::UnforceAudioThread() {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  mForcedAudioThread = false;
}

int MockCubeb::StreamInit(cubeb* aContext, cubeb_stream** aStream,
                          char const* aStreamName, cubeb_devid aInputDevice,
                          cubeb_stream_params* aInputStreamParams,
                          cubeb_devid aOutputDevice,
                          cubeb_stream_params* aOutputStreamParams,
                          cubeb_data_callback aDataCallback,
                          cubeb_state_callback aStateCallback, void* aUserPtr) {
  if (mStreamInitErrorState.compareExchange(true, false)) {
    mStreamInitEvent.Notify(nullptr);
    return CUBEB_ERROR_DEVICE_UNAVAILABLE;
  }

  auto mockStream = MakeRefPtr<SmartMockCubebStream>(
      aContext, aStreamName, aInputDevice, aInputStreamParams, aOutputDevice,
      aOutputStreamParams, aDataCallback, aStateCallback, aUserPtr,
      mRunningMode, mStreamStartFreezeEnabled);
  *aStream = mockStream->AsCubebStream();
  mStreamInitEvent.Notify(mockStream);
  // AddRef the stream to keep it alive. StreamDestroy releases it.
  NS_ADDREF(mockStream.get());
  return CUBEB_OK;
}

void MockCubeb::StreamDestroy(MockCubebStream* aStream) {
  RefPtr<SmartMockCubebStream> mockStream = dont_AddRef(aStream->mSelf);
  mStreamDestroyEvent.Notify(mockStream);
}

void MockCubeb::GoFaster() {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  mFastMode = true;
}

void MockCubeb::DontGoFaster() {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  mFastMode = false;
}

MediaEventSource<RefPtr<SmartMockCubebStream>>& MockCubeb::StreamInitEvent() {
  return mStreamInitEvent;
}

MediaEventSource<RefPtr<SmartMockCubebStream>>&
MockCubeb::StreamDestroyEvent() {
  return mStreamDestroyEvent;
}

void MockCubeb::StartStream(MockCubebStream* aStream) {
  if (aStream) {
    aStream->mMutex.AssertNotCurrentThreadOwns();
  }
  auto streams = mLiveStreams.Lock();
  if (aStream) {
    MOZ_RELEASE_ASSERT(!streams->Contains(aStream->mSelf));
    streams->AppendElement(aStream->mSelf);
  } else {
    MOZ_RELEASE_ASSERT(mForcedAudioThread);
    // Forcing an audio thread must happen before starting streams
    MOZ_RELEASE_ASSERT(streams->IsEmpty());
  }
  if (!mFakeAudioThreadRunning && mRunningMode == RunningMode::Automatic) {
    AddRef();  // released when the thread exits
    std::thread(ThreadFunction_s, this).detach();
    mFakeAudioThreadRunning = true;
  }
}

void MockCubeb::StopStream(MockCubebStream* aStream) {
  aStream->mMutex.AssertNotCurrentThreadOwns();
  {
    auto streams = mLiveStreams.Lock();
    if (!streams->Contains(aStream->mSelf)) {
      return;
    }
    streams->RemoveElement(aStream->mSelf);
  }
}

void MockCubeb::ThreadFunction() {
  MOZ_RELEASE_ASSERT(mRunningMode == RunningMode::Automatic);
  if (mForcedAudioThread) {
    mForcedAudioThreadPromise.Resolve(MakeRefPtr<AudioThreadAutoUnforcer>(this),
                                      __func__);
  }
  while (true) {
    {
      auto streams = mLiveStreams.Lock();
      for (auto& stream : *streams) {
        auto keepProcessing = stream->Process10Ms();
        if (keepProcessing == KeepProcessing::No) {
          stream = nullptr;
        }
      }
      streams->RemoveElementsBy([](const auto& stream) { return !stream; });
      MOZ_RELEASE_ASSERT(mFakeAudioThreadRunning);
      if (streams->IsEmpty() && !mForcedAudioThread) {
        mFakeAudioThreadRunning = false;
        break;
      }
    }
    std::this_thread::sleep_for(
        std::chrono::microseconds(mFastMode ? 0 : 10 * PR_USEC_PER_MSEC));
  }
  Release();
}

}  // namespace mozilla
