#include <audioapi/core/AudioParam.h>
#include <audioapi/core/BaseAudioContext.h>
#include <audioapi/core/sources/AudioBufferSourceNode.h>
#include <audioapi/core/utils/AudioGraphManager.h>
#include <audioapi/core/utils/Constants.h>
#include <audioapi/core/utils/Locker.h>
#include <audioapi/dsp/AudioUtils.hpp>
#include <audioapi/events/AudioEventHandlerRegistry.h>
#include <audioapi/types/NodeOptions.h>
#include <audioapi/utils/AudioArray.hpp>

#include <algorithm>
#include <memory>
#include <utility>

namespace audioapi {

AudioBufferSourceNode::AudioBufferSourceNode(
    const std::shared_ptr<BaseAudioContext> &context,
    const AudioBufferSourceOptions &options)
    : AudioBufferBaseSourceNode(context, options),
      loop_(options.loop),
      loopSkip_(options.loopSkip),
      loopStart_(options.loopStart),
      loopEnd_(options.loopEnd) {
  isInitialized_.store(true, std::memory_order_release);
}

void AudioBufferSourceNode::setLoop(bool loop) {
  loop_ = loop;
}

void AudioBufferSourceNode::setLoopSkip(bool loopSkip) {
  loopSkip_ = loopSkip;
}

void AudioBufferSourceNode::setLoopStart(double loopStart) {
  if (loopSkip_) {
    vReadIndex_ = loopStart * getContextSampleRate();
  }
  loopStart_ = loopStart;
}

void AudioBufferSourceNode::setLoopEnd(double loopEnd) {
  loopEnd_ = loopEnd;
}

void AudioBufferSourceNode::setBuffer(
    const std::shared_ptr<AudioBuffer> &buffer,
    const std::shared_ptr<DSPAudioBuffer> &audioBuffer) {
  std::shared_ptr<BaseAudioContext> context = context_.lock();

  if (context == nullptr) {
    return;
  }

  auto graphManager = context->getGraphManager();

  if (buffer_ != nullptr) {
    graphManager->addAudioBufferForDestruction(std::move(buffer_));
  }

  // TODO move DSPAudioBuffers destruction to graph manager as well

  if (buffer == nullptr) {
    loopEnd_ = 0;
    channelCount_ = 1;

    buffer_ = nullptr;
    return;
  }

  buffer_ = buffer;
  audioBuffer_ = audioBuffer;
  channelCount_ = static_cast<int>(buffer_->getNumberOfChannels());
  loopEnd_ = buffer_->getDuration();
}

void AudioBufferSourceNode::start(double when, double offset, double duration) {
  AudioScheduledSourceNode::start(when);

  if (duration > 0) {
    AudioScheduledSourceNode::stop(when + duration);
  }

  if (buffer_ == nullptr) {
    return;
  }

  offset = std::min(offset, static_cast<double>(buffer_->getSize()) / buffer_->getSampleRate());

  if (loop_) {
    offset = std::min(offset, loopEnd_);
  }

  vReadIndex_ = static_cast<double>(buffer_->getSampleRate() * offset);
}

void AudioBufferSourceNode::disable() {
  AudioScheduledSourceNode::disable();
}

void AudioBufferSourceNode::setOnLoopEndedCallbackId(uint64_t callbackId) {
  onLoopEndedCallbackId_ = callbackId;
}

void AudioBufferSourceNode::unregisterOnLoopEndedCallback(uint64_t callbackId) {
  audioEventHandlerRegistry_->unregisterHandler(AudioEvent::LOOP_ENDED, callbackId);
}

double AudioBufferSourceNode::getCurrentPosition() const {
  return dsp::sampleFrameToTime(static_cast<int>(vReadIndex_), buffer_->getSampleRate());
}

void AudioBufferSourceNode::sendOnLoopEndedEvent() {
  if (onLoopEndedCallbackId_ != 0) {
    audioEventHandlerRegistry_->invokeHandlerWithEventBody(
        AudioEvent::LOOP_ENDED, onLoopEndedCallbackId_, {});
  }
}

/**
 * Helper functions
 */

bool AudioBufferSourceNode::isEmpty() const {
  return buffer_ == nullptr;
}

// todo: refactor so its less complex and more readable
void AudioBufferSourceNode::processWithoutInterpolation(
    const std::shared_ptr<DSPAudioBuffer> &processingBuffer,
    size_t startOffset,
    size_t offsetLength,
    float playbackRate) {
  size_t direction = playbackRate < 0.0f ? -1 : 1;

  auto readIndex = static_cast<size_t>(vReadIndex_);
  size_t writeIndex = startOffset;

  auto frameStart = static_cast<size_t>(getVirtualStartFrame(getContextSampleRate()));
  auto frameEnd = static_cast<size_t>(getVirtualEndFrame(getContextSampleRate()));
  size_t frameDelta = frameEnd - frameStart;

  size_t framesLeft = offsetLength;

  // if we are moving towards loop, we do nothing because we will achieve it
  // otherwise, we wrap to the start of the loop if necessary
  if (loop_ &&
      ((readIndex >= frameEnd && direction == 1) || (readIndex < frameStart && direction == -1))) {
    readIndex = frameStart +
        (static_cast<int64_t>(readIndex) - static_cast<int64_t>(frameStart)) % frameDelta;
  }

  while (framesLeft > 0) {
    size_t framesToEnd = frameEnd - readIndex;
    size_t framesToCopy = std::min(framesToEnd, framesLeft);
    framesToCopy = framesToCopy > 0 ? framesToCopy : 0;

    assert(readIndex >= 0);
    assert(writeIndex >= 0);
    assert(readIndex + framesToCopy <= buffer_->getSize());
    assert(writeIndex + framesToCopy <= processingBuffer->getSize());

    // Direction is forward, we can normally copy the data
    if (direction == 1) {
      processingBuffer->copy(*buffer_, readIndex, writeIndex, framesToCopy);
    } else {
      for (size_t ch = 0; ch < processingBuffer->getNumberOfChannels(); ch += 1) {
        processingBuffer->getChannel(ch)->copyReverse(
            *buffer_->getChannel(ch), readIndex, writeIndex, framesToCopy);
      }
    }

    writeIndex += framesToCopy;
    readIndex += framesToCopy * direction;
    framesLeft -= framesToCopy;

    // if we are moving towards loop, we do nothing because we will achieve it
    // otherwise, we wrap to the start of the loop if necessary
    if ((readIndex >= frameEnd && direction == 1) || (readIndex < frameStart && direction == -1)) {
      readIndex -= direction * frameDelta;

      if (!loop_) {
        processingBuffer->zero(writeIndex, framesLeft);
        playbackState_ = PlaybackState::STOP_SCHEDULED;
        break;
      }

      sendOnLoopEndedEvent();
    }
  }

  // update reading index for next render quantum
  vReadIndex_ = static_cast<double>(readIndex);
}

void AudioBufferSourceNode::processWithInterpolation(
    const std::shared_ptr<DSPAudioBuffer> &processingBuffer,
    size_t startOffset,
    size_t offsetLength,
    float playbackRate) {
  size_t direction = playbackRate < 0.0f ? -1 : 1;

  size_t writeIndex = startOffset;

  auto vFrameStart = getVirtualStartFrame(getContextSampleRate());
  auto vFrameEnd = getVirtualEndFrame(getContextSampleRate());
  auto vFrameDelta = vFrameEnd - vFrameStart;

  auto frameStart = static_cast<size_t>(vFrameStart);
  auto frameEnd = static_cast<size_t>(vFrameEnd);

  size_t framesLeft = offsetLength;

  // Wrap to the start of the loop if necessary
  if (loop_ && (vReadIndex_ >= vFrameEnd || vReadIndex_ < vFrameStart)) {
    vReadIndex_ = vFrameStart + std::fmod(vReadIndex_ - vFrameStart, vFrameDelta);
  }

  while (framesLeft > 0) {
    auto readIndex = static_cast<size_t>(vReadIndex_);
    size_t nextReadIndex = readIndex + 1;
    auto factor = static_cast<float>(vReadIndex_ - static_cast<double>(readIndex));

    if (nextReadIndex >= frameEnd) {
      nextReadIndex = loop_ ? frameStart : readIndex;
    }

    for (size_t i = 0; i < processingBuffer->getNumberOfChannels(); i++) {
      auto destination = processingBuffer->getChannel(i)->span();
      const auto source = buffer_->getChannel(i)->span();

      destination[writeIndex] = dsp::linearInterpolate(source, readIndex, nextReadIndex, factor);
    }

    writeIndex += 1;
    vReadIndex_ += playbackRate * static_cast<double>(direction);
    framesLeft -= 1;

    if (vReadIndex_ < vFrameStart || vReadIndex_ >= vFrameEnd) {
      vReadIndex_ -= static_cast<double>(direction) * vFrameDelta;

      if (!loop_) {
        processingBuffer->zero(writeIndex, framesLeft);
        playbackState_ = PlaybackState::STOP_SCHEDULED;
        break;
      }

      sendOnLoopEndedEvent();
    }
  }
}

double AudioBufferSourceNode::getVirtualStartFrame(float sampleRate) const {
  auto loopStartFrame = loopStart_ * sampleRate;
  return loop_ && loopStartFrame >= 0 && loopStart_ < loopEnd_ ? loopStartFrame : 0.0;
}

double AudioBufferSourceNode::getVirtualEndFrame(float sampleRate) {
  auto inputBufferLength = static_cast<double>(buffer_->getSize());
  auto loopEndFrame = loopEnd_ * sampleRate;

  return loop_ && loopEndFrame > 0 && loopStart_ < loopEnd_
      ? std::min(loopEndFrame, inputBufferLength)
      : inputBufferLength;
}

} // namespace audioapi
