/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 "DelayNode.h"
#include "mozilla/dom/DelayNodeBinding.h"
#include "AudioNodeEngine.h"
#include "AudioNodeTrack.h"
#include "AudioDestinationNode.h"
#include "WebAudioUtils.h"
#include "DelayBuffer.h"
#include "PlayingRefChangeHandler.h"
#include "Tracing.h"

namespace mozilla::dom {

NS_IMPL_CYCLE_COLLECTION_INHERITED(DelayNode, AudioNode, mDelay)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DelayNode)
NS_INTERFACE_MAP_END_INHERITING(AudioNode)

NS_IMPL_ADDREF_INHERITED(DelayNode, AudioNode)
NS_IMPL_RELEASE_INHERITED(DelayNode, AudioNode)

class DelayNodeEngine final : public AudioNodeEngine {
  typedef PlayingRefChangeHandler PlayingRefChanged;

 public:
  DelayNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination,
                  float aMaxDelayTicks)
      : AudioNodeEngine(aNode),
        mDestination(aDestination->Track())
        // Keep the default value in sync with the default value in
        // DelayNode::DelayNode.
        ,
        mDelay(0.f)
        // Use a smoothing range of 20ms
        ,
        mBuffer(
            std::max(aMaxDelayTicks, static_cast<float>(WEBAUDIO_BLOCK_SIZE))),
        mMaxDelay(aMaxDelayTicks),
        mHaveProducedBeforeInput(false),
        mLeftOverData(INT32_MIN) {}

  DelayNodeEngine* AsDelayNodeEngine() override { return this; }

  enum Parameters {
    DELAY,
  };
  void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
    MOZ_ASSERT(mDestination);
    WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);

    switch (aIndex) {
      case DELAY:
        mDelay.InsertEvent<int64_t>(aEvent);
        break;
      default:
        NS_ERROR("Bad DelayNodeEngine TimelineParameter");
    }
  }

  void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
                    const AudioBlock& aInput, AudioBlock* aOutput,
                    bool* aFinished) override {
    MOZ_ASSERT(aTrack->mSampleRate == mDestination->mSampleRate);
    TRACE("DelayNodeEngine::ProcessBlock");

    if (!aInput.IsSilentOrSubnormal()) {
      if (mLeftOverData <= 0) {
        RefPtr<PlayingRefChanged> refchanged =
            new PlayingRefChanged(aTrack, PlayingRefChanged::ADDREF);
        aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
      }
      mLeftOverData = mBuffer.MaxDelayTicks();
    } else if (mLeftOverData > 0) {
      mLeftOverData -= WEBAUDIO_BLOCK_SIZE;
    } else {
      if (mLeftOverData != INT32_MIN) {
        mLeftOverData = INT32_MIN;
        aTrack->ScheduleCheckForInactive();

        // Delete our buffered data now we no longer need it
        mBuffer.Reset();

        RefPtr<PlayingRefChanged> refchanged =
            new PlayingRefChanged(aTrack, PlayingRefChanged::RELEASE);
        aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
      }
      aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
      return;
    }

    mBuffer.Write(aInput);

    // Skip output update if mLastChunks has already been set by
    // ProduceBlockBeforeInput() when in a cycle.
    if (!mHaveProducedBeforeInput) {
      UpdateOutputBlock(aTrack, aFrom, aOutput, 0.0);
    }
    mHaveProducedBeforeInput = false;
    mBuffer.NextBlock();
  }

  void UpdateOutputBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
                         AudioBlock* aOutput, float minDelay) {
    float maxDelay = mMaxDelay;
    float sampleRate = aTrack->mSampleRate;
    ChannelInterpretation channelInterpretation =
        aTrack->GetChannelInterpretation();
    if (mDelay.HasSimpleValue()) {
      // If this DelayNode is in a cycle, make sure the delay value is at least
      // one block, even if that is greater than maxDelay.
      float delayFrames = mDelay.GetValue() * sampleRate;
      float delayFramesClamped =
          std::max(minDelay, std::min(delayFrames, maxDelay));
      mBuffer.Read(delayFramesClamped, aOutput, channelInterpretation);
    } else {
      // Compute the delay values for the duration of the input AudioChunk
      // If this DelayNode is in a cycle, make sure the delay value is at least
      // one block.
      TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom);
      float values[WEBAUDIO_BLOCK_SIZE];
      mDelay.GetValuesAtTime(tick, values, WEBAUDIO_BLOCK_SIZE);

      float computedDelay[WEBAUDIO_BLOCK_SIZE];
      for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) {
        float delayAtTick = values[counter] * sampleRate;
        float delayAtTickClamped =
            std::max(minDelay, std::min(delayAtTick, maxDelay));
        computedDelay[counter] = delayAtTickClamped;
      }
      mBuffer.Read(computedDelay, aOutput, channelInterpretation);
    }
  }

  void ProduceBlockBeforeInput(AudioNodeTrack* aTrack, GraphTime aFrom,
                               AudioBlock* aOutput) override {
    if (mLeftOverData <= 0) {
      aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
    } else {
      UpdateOutputBlock(aTrack, aFrom, aOutput, WEBAUDIO_BLOCK_SIZE);
    }
    mHaveProducedBeforeInput = true;
  }

  bool IsActive() const override { return mLeftOverData != INT32_MIN; }

  size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
    size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
    // Not owned:
    // - mDestination - probably not owned
    // - mDelay - shares ref with AudioNode, don't count
    amount += mBuffer.SizeOfExcludingThis(aMallocSizeOf);
    return amount;
  }

  size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
    return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
  }

  RefPtr<AudioNodeTrack> mDestination;
  AudioParamTimeline mDelay;
  DelayBuffer mBuffer;
  float mMaxDelay;
  bool mHaveProducedBeforeInput;
  // How much data we have in our buffer which needs to be flushed out when our
  // inputs finish.
  int32_t mLeftOverData;
};

DelayNode::DelayNode(AudioContext* aContext, double aMaxDelay)
    : AudioNode(aContext, 2, ChannelCountMode::Max,
                ChannelInterpretation::Speakers) {
  mDelay = CreateAudioParam(DelayNodeEngine::DELAY, u"delayTime"_ns, 0.0f, 0.f,
                            aMaxDelay);
  DelayNodeEngine* engine = new DelayNodeEngine(
      this, aContext->Destination(), aContext->SampleRate() * aMaxDelay);
  mTrack = AudioNodeTrack::Create(
      aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
}

/* static */
already_AddRefed<DelayNode> DelayNode::Create(AudioContext& aAudioContext,
                                              const DelayOptions& aOptions,
                                              ErrorResult& aRv) {
  if (aOptions.mMaxDelayTime <= 0. || aOptions.mMaxDelayTime >= 180.) {
    aRv.ThrowNotSupportedError(
        nsPrintfCString("\"maxDelayTime\" (%g) is not in the range (0,180)",
                        aOptions.mMaxDelayTime));
    return nullptr;
  }

  RefPtr<DelayNode> audioNode =
      new DelayNode(&aAudioContext, aOptions.mMaxDelayTime);

  audioNode->Initialize(aOptions, aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  audioNode->DelayTime()->SetInitialValue(aOptions.mDelayTime);
  return audioNode.forget();
}

size_t DelayNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
  size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
  amount += mDelay->SizeOfIncludingThis(aMallocSizeOf);
  return amount;
}

size_t DelayNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
  return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
}

JSObject* DelayNode::WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) {
  return DelayNode_Binding::Wrap(aCx, this, aGivenProto);
}

}  // namespace mozilla::dom
