/* 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/. */

#ifndef DOM_MEDIA_GTEST_AUDIOVERIFIER_H_
#define DOM_MEDIA_GTEST_AUDIOVERIFIER_H_

#include <numbers>

#include "AudioGenerator.h"

namespace mozilla {

template <typename Sample>
class AudioVerifier {
 public:
  explicit AudioVerifier(uint32_t aRate, uint32_t aFrequency)
      : mRate(aRate), mFrequency(aFrequency) {}

  // Only the mono channel is taken into account.
  void AppendData(const AudioSegment& segment) {
    for (AudioSegment::ConstChunkIterator iter(segment); !iter.IsEnded();
         iter.Next()) {
      const AudioChunk& c = *iter;
      if (c.IsNull()) {
        for (int i = 0; i < c.GetDuration(); ++i) {
          CheckSample(0);
        }
      } else {
        const Sample* buffer = c.ChannelData<Sample>()[0];
        for (int i = 0; i < c.GetDuration(); ++i) {
          CheckSample(buffer[i]);
        }
      }
    }
  }

  void AppendDataInterleaved(const Sample* aBuffer, uint32_t aFrames,
                             uint32_t aChannels) {
    for (uint32_t i = 0; i < aFrames * aChannels; i += aChannels) {
      CheckSample(aBuffer[i]);
    }
  }

  float EstimatedFreq() const {
    if (mTotalFramesSoFar == PreSilenceSamples()) {
      return 0;
    }
    if (mSumPeriodInSamples == 0) {
      return 0;
    }
    if (mZeroCrossCount <= 1) {
      return 0;
    }
    return mRate /
           (static_cast<float>(mSumPeriodInSamples) / (mZeroCrossCount - 1));
  }

  // Returns the maximum difference in value between two adjacent samples along
  // the sine curve.
  Sample MaxMagnitudeDifference() {
    return static_cast<Sample>(AudioGenerator<Sample>::Amplitude() * 2 *
                               sin(2 * std::numbers::pi * mFrequency / mRate));
  }

  bool PreSilenceEnded() const {
    return mTotalFramesSoFar > mPreSilenceSamples;
  }
  uint64_t PreSilenceSamples() const { return mPreSilenceSamples; }
  uint32_t CountDiscontinuities() const { return mDiscontinuitiesCount; }

 private:
  void CheckSample(Sample aCurrentSample) {
    ++mTotalFramesSoFar;
    // Avoid pre-silence
    if (!CountPreSilence(aCurrentSample)) {
      CountZeroCrossing(aCurrentSample);
      CountDiscontinuities(aCurrentSample);
    }

    mPrevious = aCurrentSample;
  }

  bool CountPreSilence(Sample aCurrentSample) {
    if (IsZero(aCurrentSample) && mPreSilenceSamples == mTotalFramesSoFar - 1) {
      ++mPreSilenceSamples;
      return true;
    }
    if (IsZero(mPrevious) && aCurrentSample > 0 &&
        aCurrentSample < 2 * MaxMagnitudeDifference() &&
        mPreSilenceSamples == mTotalFramesSoFar - 1) {
      // Previous zero considered the first sample of the waveform.
      --mPreSilenceSamples;
    }
    return false;
  }

  // Positive to negative direction
  void CountZeroCrossing(Sample aCurrentSample) {
    if (mPrevious > 0 && aCurrentSample <= 0) {
      if (mZeroCrossCount++) {
        MOZ_RELEASE_ASSERT(mZeroCrossCount > 1);
        mSumPeriodInSamples += mTotalFramesSoFar - mLastZeroCrossPosition;
      }
      mLastZeroCrossPosition = mTotalFramesSoFar;
    }
  }

  void CountDiscontinuities(Sample aCurrentSample) {
    // The factor of 2 tolerates up to 1 skipped frame.
    const bool haveDiscontinuity =
        fabs(aCurrentSample - mPrevious) > 2 * MaxMagnitudeDifference();

    if (mCurrentDiscontinuityFrameCount > 0) {
      if (++mCurrentDiscontinuityFrameCount == 5) {
        // Allow a grace-period of 5 samples for any given discontinuity.
        // For instance the speex resampler can smooth out a sudden drop to 0
        // over several samples.
        mCurrentDiscontinuityFrameCount = 0;
      }
      return;
    }

    MOZ_RELEASE_ASSERT(mCurrentDiscontinuityFrameCount == 0);
    if (!haveDiscontinuity) {
      return;
    }

    // Encountered a new discontinuity.
    ++mCurrentDiscontinuityFrameCount;
    ++mDiscontinuitiesCount;
  }

  bool IsZero(float aValue) { return fabs(aValue) < 1e-8; }
  bool IsZero(short aValue) { return aValue == 0; }

 private:
  const uint32_t mRate;
  const uint32_t mFrequency;

  uint32_t mZeroCrossCount = 0;
  uint64_t mLastZeroCrossPosition = 0;
  uint64_t mSumPeriodInSamples = 0;

  uint64_t mTotalFramesSoFar = 0;
  uint64_t mPreSilenceSamples = 0;

  uint32_t mCurrentDiscontinuityFrameCount = 0;
  uint32_t mDiscontinuitiesCount = 0;
  // This is needed to connect the previous buffers.
  Sample mPrevious = {};
};

}  // namespace mozilla

#endif  // DOM_MEDIA_GTEST_AUDIOVERIFIER_H_
