// Copyright (c) 2019, Paul Ferrand
// All rights reserved.

// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:

// 1. Redistributions of source code must retain the above copyright notice, this
//    list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.

// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "Voice.h"
#include "AudioSpan.h"
#include "Config.h"
#include "Defaults.h"
#include "MathHelpers.h"
#include "SIMDHelpers.h"
#include "SfzHelpers.h"
#include "absl/algorithm/container.h"
#include <memory>

sfz::Voice::Voice(const CCValueArray& ccState)
    : ccState(ccState)
{
}

void sfz::Voice::startVoice(Region* region, int delay, int channel, int number, uint8_t value, sfz::Voice::TriggerType triggerType) noexcept
{
    this->triggerType = triggerType;
    triggerNumber = number;
    triggerChannel = channel;
    triggerValue = value;

    this->region = region;

    ASSERT(delay >= 0);
    if (delay < 0)
        delay = 0;

    // DBG("Starting voice with " << region->sample);

    state = State::playing;
    speedRatio = static_cast<float>(region->sampleRate / this->sampleRate);
    pitchRatio = region->getBasePitchVariation(number, value);

    baseVolumedB = region->getBaseVolumedB(number);

    auto volumedB { baseVolumedB };
    if (region->volumeCC)
        volumedB += normalizeCC(ccState[region->volumeCC->first]) * region->volumeCC->second;
    volumeEnvelope.reset(db2mag(volumedB));
    // DBG("Base volume: " << baseVolumedB << " dB - with modifier: " << volumedB << " dB");

    baseGain = region->getBaseGain();
    baseGain *= region->getCrossfadeGain(ccState);
    if (triggerType != TriggerType::CC)
        baseGain *= region->getNoteGain(number, value);

    float gain { baseGain };
    if (region->amplitudeCC)
        gain *= normalizeCC(ccState[region->amplitudeCC->first]) * normalizePercents(region->amplitudeCC->second);
    amplitudeEnvelope.reset(gain);
    // DBG("Base gain: " << baseGain << " - with modifier: " << gain);

    basePan = normalizeNegativePercents(region->pan);
    auto pan { basePan };
    if (region->panCC)
        pan += normalizeCC(ccState[region->panCC->first]) * normalizeNegativePercents(region->panCC->second);
    panEnvelope.reset(pan);
    // DBG("Base pan: " << basePan << " - with modifier: " << pan);

    basePosition = normalizeNegativePercents(region->position);
    auto position { basePosition };
    if (region->positionCC)
        position += normalizeCC(ccState[region->positionCC->first]) * normalizeNegativePercents(region->positionCC->second);
    positionEnvelope.reset(position);
    // DBG("Base position: " << basePosition << " - with modifier: " << position);

    baseWidth = normalizeNegativePercents(region->width);
    auto width { baseWidth };
    if (region->widthCC)
        width += normalizeCC(ccState[region->widthCC->first]) * normalizeNegativePercents(region->widthCC->second);
    widthEnvelope.reset(width);
    // DBG("Base width: " << baseWidth << " - with modifier: " << width);

    sourcePosition = region->getOffset();
    // DBG("Offset: " << sourcePosition);
    initialDelay = delay + static_cast<uint32_t>(region->getDelay() / sampleRate);
    baseFrequency = midiNoteFrequency(number) * pitchRatio;
    prepareEGEnvelope(delay, value);
}

void sfz::Voice::prepareEGEnvelope(int delay, uint8_t velocity) noexcept
{
    auto secondsToSamples = [this](auto timeInSeconds) {
        return static_cast<int>(timeInSeconds * sampleRate);
    };

    egEnvelope.reset(
        secondsToSamples(region->amplitudeEG.getAttack(ccState, velocity)),
        secondsToSamples(region->amplitudeEG.getRelease(ccState, velocity)),
        normalizePercents(region->amplitudeEG.getSustain(ccState, velocity)),
        delay + secondsToSamples(region->amplitudeEG.getDelay(ccState, velocity)),
        secondsToSamples(region->amplitudeEG.getDecay(ccState, velocity)),
        secondsToSamples(region->amplitudeEG.getHold(ccState, velocity)),
        normalizePercents(region->amplitudeEG.getStart(ccState, velocity)));
}

void sfz::Voice::setFileData(std::shared_ptr<AudioBuffer<float>> file, unsigned ticket) noexcept
{
    if (ticket != this->ticket)
        return;

    fileData = std::move(file);
    dataReady.store(true);
}

bool sfz::Voice::isFree() const noexcept
{
    return (region == nullptr);
}

void sfz::Voice::release(int delay) noexcept
{
    if (state == State::playing) {
        state = State::release;
        egEnvelope.startRelease(delay);
    }
}

void sfz::Voice::registerNoteOff(int delay, int channel, int noteNumber, uint8_t velocity [[maybe_unused]]) noexcept
{
    if (region == nullptr)
        return;

    if (state != State::playing)
        return;

    if (triggerChannel == channel && triggerNumber == noteNumber) {
        noteIsOff = true;

        if (region->loopMode == SfzLoopMode::one_shot)
            return;

        if (!region->checkSustain || ccState[config::sustainCC] < config::halfCCThreshold)
            release(delay);
    }
}

void sfz::Voice::registerCC(int delay, int channel [[maybe_unused]], int ccNumber, uint8_t ccValue) noexcept
{
    if (region == nullptr)
        return;

    if (region->checkSustain && noteIsOff && ccNumber == config::sustainCC && ccValue < config::halfCCThreshold)
        release(delay);

    if (region->amplitudeCC && ccNumber == region->amplitudeCC->first) {
        const float newGain { baseGain * normalizeCC(ccValue) * normalizePercents(region->amplitudeCC->second) };
        amplitudeEnvelope.registerEvent(delay, newGain);
    }

    if (region->volumeCC && ccNumber == region->volumeCC->first) {
        const float newVolumedB { baseVolumedB + normalizeCC(ccValue) * region->volumeCC->second };
        amplitudeEnvelope.registerEvent(delay, db2mag(newVolumedB));
    }

    if (region->panCC && ccNumber == region->panCC->first) {
        const float newPan { basePan + normalizeCC(ccValue) * normalizeNegativePercents(region->panCC->second) };
        panEnvelope.registerEvent(delay, newPan);
    }

    if (region->positionCC && ccNumber == region->positionCC->first) {
        const float newPosition { basePosition + normalizeCC(ccValue) * normalizeNegativePercents(region->positionCC->second) };
        positionEnvelope.registerEvent(delay, newPosition);
    }

    if (region->widthCC && ccNumber == region->widthCC->first) {
        const float newWidth { baseWidth + normalizeCC(ccValue) * normalizeNegativePercents(region->widthCC->second) };
        widthEnvelope.registerEvent(delay, newWidth);
    }
}

void sfz::Voice::registerPitchWheel(int delay [[maybe_unused]], int channel [[maybe_unused]], int pitch [[maybe_unused]]) noexcept
{
    // TODO
}

void sfz::Voice::registerAftertouch(int delay [[maybe_unused]], int channel [[maybe_unused]], uint8_t aftertouch [[maybe_unused]]) noexcept
{
    // TODO
}

void sfz::Voice::registerTempo(int delay [[maybe_unused]], float secondsPerQuarter [[maybe_unused]]) noexcept
{
    // TODO
}

void sfz::Voice::setSampleRate(float sampleRate) noexcept
{
    this->sampleRate = sampleRate;
}

void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept
{
    this->samplesPerBlock = samplesPerBlock;
    tempBuffer1.resize(samplesPerBlock);
    tempBuffer2.resize(samplesPerBlock);
    tempBuffer3.resize(samplesPerBlock);
    indexBuffer.resize(samplesPerBlock);
    tempSpan1 = absl::MakeSpan(tempBuffer1);
    tempSpan2 = absl::MakeSpan(tempBuffer2);
    tempSpan3 = absl::MakeSpan(tempBuffer3);
    indexSpan = absl::MakeSpan(indexBuffer);
}

void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
{
    ASSERT(static_cast<int>(buffer.getNumFrames()) <= samplesPerBlock);
    buffer.fill(0.0f);

    if (state == State::idle || region == nullptr) {
        powerHistory.push(0.0);
        return;
    }

    if (region->isGenerator())
        fillWithGenerator(buffer);
    else
        fillWithData(buffer);

    if (region->isStereo())
        processStereo(buffer);
    else
        processMono(buffer);

    if (!egEnvelope.isSmoothing())
        reset();

    powerHistory.push(buffer.meanSquared());
}

void sfz::Voice::processMono(AudioSpan<float> buffer) noexcept
{
    const auto numSamples = buffer.getNumFrames();
    auto leftBuffer = buffer.getSpan(0);
    auto rightBuffer = buffer.getSpan(1);

    auto span1 = tempSpan1.first(numSamples);
    auto span2 = tempSpan2.first(numSamples);

    // Amplitude envelope
    amplitudeEnvelope.getBlock(span1);
    ::applyGain<float>(span1, leftBuffer);

    // AmpEG envelope
    egEnvelope.getBlock(span1);
    ::applyGain<float>(span1, leftBuffer);

    // Volume envelope
    volumeEnvelope.getBlock(span1);
    ::applyGain<float>(span1, leftBuffer);

    // Prepare for stereo output
    ::copy<float>(leftBuffer, rightBuffer);

    panEnvelope.getBlock(span1);
    // We assume that the pan envelope is already normalized between -1 and 1
    // Check bm_pan for your architecture to check if it's interesting to use the pan helper instead
    ::fill<float>(span2, 1.0f);
    ::add<float>(span1, span2);
    ::applyGain<float>(piFour<float>, span2);
    ::cos<float>(span2, span1);
    ::sin<float>(span2, span2);
    ::applyGain<float>(span1, leftBuffer);
    ::applyGain<float>(span2, rightBuffer);
}

void sfz::Voice::processStereo(AudioSpan<float> buffer) noexcept
{
    const auto numSamples = buffer.getNumFrames();
    auto span1 = tempSpan1.first(numSamples);
    auto span2 = tempSpan2.first(numSamples);
    auto span3 = tempSpan3.first(numSamples);
    auto leftBuffer = buffer.getSpan(0);
    auto rightBuffer = buffer.getSpan(1);

    // Amplitude envelope
    amplitudeEnvelope.getBlock(span1);
    buffer.applyGain(span1);

    // AmpEG envelope
    egEnvelope.getBlock(span1);
    buffer.applyGain(span1);

    // Volume envelope
    volumeEnvelope.getBlock(span1);
    buffer.applyGain(span1);

    // Create mid/side from left/right in the output buffer
    ::copy<float>(rightBuffer, span1);
    ::add<float>(leftBuffer, rightBuffer);
    ::subtract<float>(span1, leftBuffer);
    ::applyGain<float>(sqrtTwoInv<float>, leftBuffer);
    ::applyGain<float>(sqrtTwoInv<float>, rightBuffer);

    // Apply the width process
    widthEnvelope.getBlock(span1);
    ::fill<float>(span2, 1.0f);
    ::add<float>(span1, span2);
    ::applyGain<float>(piFour<float>, span2);
    ::cos<float>(span2, span1);
    ::sin<float>(span2, span2);
    ::applyGain<float>(span1, leftBuffer);
    ::applyGain<float>(span2, rightBuffer);

    // Apply a position to the "left" channel which is supposed to be our mid channel
    // TODO: add panning here too?
    positionEnvelope.getBlock(span1);
    ::fill<float>(span2, 1.0f);
    ::add<float>(span1, span2);
    ::applyGain<float>(piFour<float>, span2);
    ::cos<float>(span2, span1);
    ::sin<float>(span2, span2);
    ::copy<float>(leftBuffer, span3);
    ::copy<float>(rightBuffer, leftBuffer);
    ::multiplyAdd<float>(span1, span3, leftBuffer);
    ::multiplyAdd<float>(span2, span3, rightBuffer);
    ::applyGain<float>(sqrtTwoInv<float>, leftBuffer);
    ::applyGain<float>(sqrtTwoInv<float>, rightBuffer);
}

void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
{
    auto source { [&]() {
        if (region->canUsePreloadedData())
            return AudioSpan<const float>(*region->preloadedData);
        else if (!dataReady)
            return AudioSpan<const float>(*region->preloadedData);
        else
            return AudioSpan<const float>(*fileData);
    }() };

    auto indices = indexSpan.first(buffer.getNumFrames());
    auto jumps = tempSpan1.first(buffer.getNumFrames());
    auto leftCoeffs = tempSpan1.first(buffer.getNumFrames());
    auto rightCoeffs = tempSpan2.first(buffer.getNumFrames());

    ::fill<float>(jumps, pitchRatio * speedRatio);
    jumps[0] += floatPositionOffset;
    ::cumsum<float>(jumps, jumps);
    ::sfzInterpolationCast<float>(jumps, indices, leftCoeffs, rightCoeffs);
    ::add<int>(sourcePosition, indices);

    //FIXME : all this casting is driving me crazy
    const auto sampleEnd = min(static_cast<int>(region->trueSampleEnd()), static_cast<int>(source.getNumFrames())) - 1;
    if (region->shouldLoop() && region->loopRange.getEnd() <= source.getNumFrames()) {
        const auto offset = sampleEnd - static_cast<int>(region->loopRange.getStart());
        for (auto* index = indices.begin(); index < indices.end(); ++index) {
            if (*index > sampleEnd) {
                const auto remainingElements = static_cast<size_t>(std::distance(index, indices.end()));
                ::subtract<int>(offset, { index, remainingElements });
            }
        }
    } else {
        for (auto* index = indices.begin(); index < indices.end(); ++index) {
            if (*index > sampleEnd) {
                const auto remainingElements = static_cast<size_t>(std::distance(index, indices.end()));
                ::fill<int>(indices.last(remainingElements), sampleEnd);
                ::fill<float>(leftCoeffs.last(remainingElements), 0.0f);
                ::fill<float>(rightCoeffs.last(remainingElements), 1.0f);
                break;
            }
        }
    }

    auto ind = indices.data();
    auto leftCoeff = leftCoeffs.data();
    auto rightCoeff = rightCoeffs.data();
    auto left = buffer.getChannel(0);
    auto leftSource = source.getChannel(0);
    if (source.getNumChannels() == 1) {
        while (ind < indices.end()) {
            *left = leftSource[*ind] * (*leftCoeff) + leftSource[*ind + 1] * (*rightCoeff);
            left++;
            ind++;
            leftCoeff++;
            rightCoeff++;
        }
    } else {
        auto right = buffer.getChannel(1);
        auto rightSource = source.getChannel(1);
        while (ind < indices.end()) {
            *left = leftSource[*ind] * (*leftCoeff) + leftSource[*ind + 1] * (*rightCoeff);
            *right = rightSource[*ind] * (*leftCoeff) + rightSource[*ind + 1] * (*rightCoeff);
            left++;
            right++;
            ind++;
            leftCoeff++;
            rightCoeff++;
        }
    }

    sourcePosition = indices.back();
    floatPositionOffset = rightCoeffs.back();

    if (state != State::release && !region->shouldLoop() && sourcePosition == sampleEnd) {
        DBG("Releasing " << region->sample);
        auto last = std::distance(indices.begin(), absl::c_find(indices, sampleEnd));
        release(last);
        buffer.subspan(last).fill(0.0f);
    }
}

void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept
{
    if (region->sample != "*sine")
        return;

    float step = baseFrequency * twoPi<float> / sampleRate;
    phase = ::linearRamp<float>(tempSpan1, phase, step);

    ::sin<float>(tempSpan1.first(buffer.getNumFrames()), buffer.getSpan(0));
    ::copy<float>(buffer.getSpan(0), buffer.getSpan(1));

    // Wrap the phase so we don't loose too much precision on longer notes
    const auto numTwoPiWraps = static_cast<int>(phase / twoPi<float>);
    phase -= twoPi<float> * static_cast<float>(numTwoPiWraps);
}

bool sfz::Voice::checkOffGroup(int delay, uint32_t group) noexcept
{
    if (region != nullptr && triggerType == TriggerType::NoteOn && region->offBy && *region->offBy == group) {
        DBG("Off group of sample " << region->sample);
        release(delay);
        return true;
    }

    return false;
}

int sfz::Voice::getTriggerNumber() const noexcept
{
    return triggerNumber;
}

int sfz::Voice::getTriggerChannel() const noexcept
{
    return triggerChannel;
}

uint8_t sfz::Voice::getTriggerValue() const noexcept
{
    return triggerValue;
}

sfz::Voice::TriggerType sfz::Voice::getTriggerType() const noexcept
{
    return triggerType;
}

void sfz::Voice::reset() noexcept
{
    dataReady.store(false);
    fileData.reset();
    state = State::idle;
    if (region != nullptr) {
        DBG("Reset voice with sample " << region->sample);
    }
    region = nullptr;
    sourcePosition = 0;
    floatPositionOffset = 0.0f;
    noteIsOff = false;
}

void sfz::Voice::garbageCollect() noexcept
{
    if (state == State::idle && region == nullptr) {
        fileData.reset();
    }
}

void sfz::Voice::expectFileData(unsigned ticket)
{
    this->ticket = ticket;
}

float sfz::Voice::getMeanSquaredAverage() const noexcept
{
    return powerHistory.getAverage();
}

bool sfz::Voice::canBeStolen() const noexcept
{
    return state == State::release;
}

uint32_t sfz::Voice::getSourcePosition() const noexcept
{
    return sourcePosition;
}
