/*
    scsynth-rs: a host-owned audio driver for scsynth.

    This is the only modification to SuperCollider in scsynth-rs. It is NOT part of the upstream
    tree: a small build-time patch to `server/scsynth/CMakeLists.txt` adds an `AUDIOAPI=external`
    branch that compiles this file and defines `SC_AUDIO_API=SC_AUDIO_API_EXTERNAL`.

    Unlike the device-owning backends (CoreAudio/JACK/PortAudio), this driver owns no audio
    thread and no device: the host (Rust, e.g. a cpal callback) pumps it one buffer at a time by
    calling `scsynth_pump`. It is modelled on `SC_WebAudio.cpp`'s externally-driven `processBlock`
    but takes interleaved float buffers, so it suits any host audio loop.
*/

#if SC_AUDIO_API == SC_AUDIO_API_EXTERNAL

#    include "SC_CoreAudio.h"
#    include "SC_HiddenWorld.h"
#    include "SC_Prototypes.h"
#    include "SC_Time.hpp"
#    include "SC_World.h"

// Per-backend time helpers. Each audio backend (CoreAudio/JACK/PortAudio/...) provides its own;
// since we replace them, we must too. These mirror PortAudio's wall-clock implementations.
extern "C" int32 server_timeseed() { return timeSeed(); }
extern "C" int64 oscTimeNow() { return OSCTime(getTime()); }
void initializeScheduler() {}

class SC_ExternalDriver : public SC_AudioDriver {
public:
    SC_ExternalDriver(World* inWorld): SC_AudioDriver(inWorld) { mOSCbuftime = 0; }
    ~SC_ExternalDriver() override = default;

    // Run `numFrames` of synthesis, reading interleaved input and writing interleaved output.
    // `numFrames` should be a whole multiple of the world's block size; any remainder is zeroed.
    void processBlock(const float* input, int numInputChannels, float* output, int numOutputChannels,
                      int numFrames);

protected:
    bool DriverSetup(int* outNumSamplesPerCallback, double* outSampleRate) override {
        // The host pumps arbitrary buffer sizes; report the preferred values purely for the
        // engine's bookkeeping (sample rate, CPU stats). Default the block to the control-block
        // size when the host expresses no preference.
        *outSampleRate =
            mPreferredSampleRate != 0 ? (double)mPreferredSampleRate : (double)mWorld->mBufLength * 1.0;
        if (mPreferredSampleRate == 0)
            *outSampleRate = 44100.0;
        *outNumSamplesPerCallback =
            mPreferredHardwareBufferFrameSize != 0 ? (int)mPreferredHardwareBufferFrameSize : (int)mWorld->mBufLength;
        return true;
    }
    bool DriverStart() override { return true; }
    bool DriverStop() override { return true; }
};

SC_AudioDriver* SC_NewAudioDriver(World* inWorld) { return new SC_ExternalDriver(inWorld); }

void SC_ExternalDriver::processBlock(const float* input, int numInputChannels, float* output, int numOutputChannels,
                                     int numFrames) {
    World* world = mWorld;

    // Drain the cross-thread FIFOs (NRT thread hand-offs, freshly received OSC). Mirrors the
    // other drivers' per-callback prologue.
    mFromEngine.Free();
    mToEngine.Perform();
    mOscPacketsToEngine.Perform();

    const int bufFrames = (int)world->mBufLength;
    const int numBufs = numFrames / bufFrames;

    float* inBuses = world->mAudioBus + world->mNumOutputs * bufFrames;
    const float* outBuses = world->mAudioBus;
    int32* inTouched = world->mAudioBusTouched + world->mNumOutputs;
    const int32* outTouched = world->mAudioBusTouched;

    int64 oscTime = mOSCbuftime;
    const int64 oscInc = mOSCincrement;

    int frameOffset = 0;
    for (int i = 0; i < numBufs; ++i, world->mBufCounter++, frameOffset += bufFrames) {
        const int32 bufCounter = world->mBufCounter;

        // Deinterleave host input into the input buses.
        for (int ch = 0; input && ch < (int)world->mNumInputs && ch < numInputChannels; ++ch) {
            float* dst = inBuses + ch * bufFrames;
            const float* src = input + frameOffset * numInputChannels + ch;
            for (int k = 0; k < bufFrames; ++k)
                dst[k] = src[k * numInputChannels];
            inTouched[ch] = bufCounter;
        }

        // Perform any scheduled (timestamped) events due within this block.
        const int64 nextTime = oscTime + oscInc;
        while (mScheduler.NextTime() <= nextTime) {
            SC_ScheduledEvent event = mScheduler.Remove();
            event.Perform();
        }
        world->mSampleOffset = 0;
        world->mSubsampleOffset = 0.f;

        World_Run(world);

        // Interleave the output buses into the host output buffer.
        for (int ch = 0; ch < (int)world->mNumOutputs && ch < numOutputChannels; ++ch) {
            float* dst = output + frameOffset * numOutputChannels + ch;
            if (outTouched[ch] == bufCounter) {
                const float* srcBus = outBuses + ch * bufFrames;
                for (int k = 0; k < bufFrames; ++k)
                    dst[k * numOutputChannels] = srcBus[k];
            } else {
                for (int k = 0; k < bufFrames; ++k)
                    dst[k * numOutputChannels] = 0.f;
            }
        }

        oscTime = mOSCbuftime = nextTime;
    }

    // Zero any trailing frames that did not make up a whole control block.
    for (int f = numBufs * bufFrames; f < numFrames; ++f)
        for (int ch = 0; ch < numOutputChannels; ++ch)
            output[f * numOutputChannels + ch] = 0.f;

    // Wake the NRT helper thread so it flushes replies and runs async command stages.
    mAudioSync.Signal();
}

// The host's entry point: pump `numFrames` of audio through the world's external driver.
extern "C" void scsynth_pump(World* world, const float* input, int numInputChannels, float* output,
                             int numOutputChannels, int numFrames) {
    if (!world || !world->hw || !world->hw->mAudioDriver)
        return;
    auto* driver = static_cast<SC_ExternalDriver*>(world->hw->mAudioDriver);
    driver->processBlock(input, numInputChannels, output, numOutputChannels, numFrames);
}

#endif // SC_AUDIO_API == SC_AUDIO_API_EXTERNAL
