// See bake_render.h.

#include "core/instrument/bake/bake_render.h"

#include <algorithm>
#include <cmath>

#include "core/instrument/engine/limiter.h"
#include "core/instrument/engine/voice_engine.h"

namespace reasampler::instrument::bake {

namespace {

// A fixed render block rather than the host's. A block boundary is where the engine
// re-observes live state, and the detach below leaves it nothing to observe — so this is
// defence in depth against a future block-boundary read, not the reason two bakes agree.
constexpr std::int64_t kBlockFrames = 512;

}  // namespace

BakeAudio renderBake(SampleData sample, const BakePlan& plan, double masterGainLinear,
                     bool limiterEnabled) {
    BakeAudio out;
    if (!sample.playable() || plan.totalFrames <= 0 || plan.sampleRate <= 0) return out;
    // Each field bounded BEFORE the sum: renderFrames() adds them, and a hand-built plan
    // (planBake already bounds both — bake_plan.cpp) could otherwise carry leadInFrames
    // near INT64_MAX and signed-overflow inside the guard meant to catch exactly that.
    if (plan.leadInFrames < 0 || plan.leadInFrames > kMaxBakeFrames ||
        plan.totalFrames > kMaxBakeFrames) {
        return out;
    }
    if (plan.renderFrames() > kMaxBakeFrames) return out;

    // The live block is the audio thread's moving target; a render that observed it would
    // depend on what the user happened to be dragging. The dialed values are already in
    // this SampleData's own play params, which is what the bake is meant to print.
    sample.live = nullptr;

    const int channels = sample.channelCount();
    // The limiter delays its output by its lookahead, so the buffers carry that many extra
    // frames and the window is read that far in — the file is the same frames it would be
    // with the limiter bypassed, not the capture shifted late by 2 ms. The extra input is
    // SILENCE rather than more rendered audio: the file ends at the window, so a peak past
    // it is not in the capture and must not duck the frames that are.
    const auto flushFrames = static_cast<std::size_t>(
        limiterEnabled ? engine::limiterLookaheadSamples(plan.sampleRate) : 0);
    const auto rendered = static_cast<std::size_t>(plan.renderFrames());
    std::vector<AudioSample> left(rendered + flushFrames, 0.f);
    std::vector<AudioSample> right(channels == 2 ? rendered + flushFrames : 0u, 0.f);

    // Pre-size the Preserve shifters here, off any audio thread, exactly as the processor
    // does for its live engine — a cold shifter would smear the onset.
    std::int64_t preserveWindow = static_cast<std::int64_t>(
        kPreserveWindowMs * static_cast<double>(plan.sampleRate) / 1000.0 + 0.5);
    if (preserveWindow < 2) preserveWindow = 2;
    VoiceEngine engine(/*maxVoices=*/1, sample, /*preserveVoiceCap=*/0, preserveWindow,
                       VoiceMode::Poly, MonoTrigger::Retrigger, /*takeoverDeclick=*/false);

    for (std::int64_t pos = 0; pos < plan.renderFrames();) {
        if (pos == plan.noteOnFrame) engine.noteOn(plan.note, plan.velocity);
        // Trigger ignores note-off by design; in Gate this is the release the programmed
        // note length bounds.
        if (pos == plan.noteOffFrame) engine.noteOff(plan.note);

        // Stop the block at the next event frame so both land sample-accurately. An event
        // past the window (a capture that closes before the note) never bounds anything.
        std::int64_t limit = plan.renderFrames();
        if (pos < plan.noteOnFrame) limit = (std::min)(limit, plan.noteOnFrame);
        else if (pos < plan.noteOffFrame) limit = (std::min)(limit, plan.noteOffFrame);
        const std::int64_t chunk = (std::min)(limit - pos, kBlockFrames);
        if (chunk <= 0) break;  // unreachable while limit > pos; a guard, not a path

        const auto at = static_cast<std::size_t>(pos);
        const auto n = static_cast<std::size_t>(chunk);
        if (channels == 2) engine.render(left.data() + at, right.data() + at, n);
        else engine.render(left.data() + at, n);
        pos += chunk;
    }

    // The whole master stage is printed here rather than left for the processor, in the
    // processor's own order — gain, then the limiter — because resetAfterBake hands both
    // controls back neutral: a render that only summed voices would return every iteration
    // shifted by 1/gain and unlimited, and a gain dialed to silence would come back at full
    // level. A flat gain multiply, not the processor's per-sample ramp: the gain is constant
    // for the whole render, which is exactly what that ramp exists to converge to.
    const auto gain = static_cast<AudioSample>(masterGainLinear);
    for (AudioSample& s : left) s *= gain;
    for (AudioSample& s : right) s *= gain;

    if (limiterEnabled) {
        engine::Limiter limiter;
        // Enabled BEFORE prepare, whose reset snaps to the enable target: that starts the
        // render already engaged. Enabling afterwards takes process()'s live-engage path,
        // which mutes for the delay-line prime and then fades in — silencing the head of the
        // capture. prepare()'s allocation and transcendentals are legal here: the bake runs
        // on the UI thread, never in process().
        limiter.setEnabled(true);
        limiter.prepare(plan.sampleRate);
        // One call: kMaxBakeFrames bounds the whole buffer well inside int, and a block
        // split would change nothing (the limiter carries its state across calls).
        limiter.process(left.data(), channels == 2 ? right.data() : nullptr,
                        static_cast<int>(left.size()));
    }

    out.channelCount = channels;
    out.sampleRate = plan.sampleRate;
    const auto lead = static_cast<std::size_t>(plan.leadInFrames) + flushFrames;
    const auto total = static_cast<std::size_t>(plan.totalFrames);
    out.interleaved.resize(total * static_cast<std::size_t>(channels));
    for (std::size_t f = 0; f < total; ++f) {
        out.interleaved[f * channels] = left[lead + f];
        if (channels == 2) out.interleaved[f * channels + 1] = right[lead + f];
    }
    return out;
}

}  // namespace reasampler::instrument::bake
