/*
    SuperCollider real time audio synthesis system
    Copyright (c) 2002 James McCartney. All rights reserved.
    http://www.audiosynth.com

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
*/


#include "SC_SequencedCommand.h"
#include "SC_CoreAudio.h"
#include "SC_Errors.h"
#include "scsynthsend.h"
#include "SC_Prototypes.h"
#include "SC_HiddenWorld.h"
#include "SC_Filesystem.hpp"
#include "SC_StringParser.h"
#include "../../common/SC_SndFileHelpers.hpp"
#include "SC_WorldOptions.h"

#include <filesystem>

const size_t ERR_BUF_SIZE(512);

#define ReturnSCErrIfNil(ptr)                                                                                          \
    if (!ptr)                                                                                                          \
    return kSCErr_OutOfRealTimeMemory
#define ThrowIfNil(ptr)                                                                                                \
    if (!ptr)                                                                                                          \
        throw std::runtime_error(                                                                                      \
            std::string("alloc failed, increase server's memory allocation (e.g. via ServerOptions)"));

#define GET_COMPLETION_MSG(msg)                                                                                        \
    mMsgSize = msg.getbsize();                                                                                         \
    if (mMsgSize) {                                                                                                    \
        mMsgData = (char*)World_Alloc(mWorld, mMsgSize);                                                               \
        ReturnSCErrIfNil(mMsgData);                                                                                    \
        msg.getb(mMsgData, mMsgSize);                                                                                  \
    } else if (msg.nextTag() == 'i') {                                                                                 \
        msg.geti(0);                                                                                                   \
    }

void PerformCompletionMsg(World* inWorld, OSC_Packet* inPacket);

#define SEND_COMPLETION_MSG                                                                                            \
    if (mMsgSize) {                                                                                                    \
        OSC_Packet packet;                                                                                             \
        packet.mData = mMsgData;                                                                                       \
        packet.mSize = mMsgSize;                                                                                       \
        packet.mIsBundle = false;                                                                                      \
        packet.mReplyAddr = mReplyAddress;                                                                             \
        PacketStatus status = PerformCompletionMsg(mWorld, packet);                                                    \
        if (status == PacketScheduled) {                                                                               \
            mMsgSize = 0;                                                                                              \
            mMsgData = 0;                                                                                              \
        }                                                                                                              \
    }

void SndBuf_Init(SndBuf* buf);
void SndBuf_Init(SndBuf* buf) {
    buf->data = nullptr;
    buf->channels = 0;
    buf->samples = 0;
    buf->frames = 0;
    buf->mask = 0;
    buf->mask1 = 0;
    buf->coord = 0;
    buf->sndfile = nullptr;
}

char* allocAndRestrictPath(World* mWorld, const char* inPath, const char* restrictBase);
char* allocAndRestrictPath(World* mWorld, const char* inPath, const char* restrictBase) {
    char strbuf[PATH_MAX];
    int offset = 0;
    int remain = PATH_MAX;

    // Ensure begins with the base
    if (strncmp(inPath, restrictBase, strlen(restrictBase)) != 0) {
        strcpy(strbuf, restrictBase);
        offset = strlen(restrictBase);
        remain -= offset;
        if (inPath[0] != '/' && strbuf[strlen(strbuf) - 1] != '/') {
            strbuf[offset] = '/';
            ++offset;
            --remain;
        }
    }

    // Now copy string, but discard any ".." (which could be benign, but easy to abuse)
    SC_StringParser sp(inPath, '/');
    size_t tokenlen;
    while (!sp.AtEnd()) {
        const char* token = const_cast<char*>(sp.NextToken());
        tokenlen = strlen(token);
        // now add the new token, then a slash, as long as token is neither dodgy nor overflows
        if (strcmp(token, "..") != 0 && remain > tokenlen) {
            strcpy(strbuf + offset, token);
            offset += tokenlen;
            remain -= tokenlen;
            if (!sp.AtEnd()) {
                strbuf[offset] = '/';
                ++offset;
                --remain;
            }
        }
    }

    // Now we can make a long-term home for the string and return it
    char* saferPath = (char*)World_Alloc(mWorld, strlen(strbuf) + 1);
    ThrowIfNil(saferPath);
    strcpy(saferPath, strbuf);
    return saferPath;
}

SC_SequencedCommand::SC_SequencedCommand(World* inWorld, ReplyAddress* inReplyAddress):
    mNextStage(1),
    mWorld(inWorld),
    mMsgSize(0),
    mMsgData(nullptr) {
    if (inReplyAddress)
        mReplyAddress = *inReplyAddress;
    else
        mReplyAddress.mReplyFunc = null_reply_func;
}

SC_SequencedCommand::~SC_SequencedCommand() {
    if (mMsgData)
        World_Free(mWorld, mMsgData);
}

int SC_SequencedCommand::Init(char* /*inData*/, int /*inSize*/) { return kSCErr_None; }

void SC_SequencedCommand::SendDone(const char* inCommandName) { ::SendDone(&mReplyAddress, inCommandName); };

void SC_SequencedCommand::SendDoneWithIntValue(const char* inCommandName, int value) {
    ::SendDoneWithIntValue(&mReplyAddress, inCommandName, value);
};

void SC_SequencedCommand::CallEveryStage() {
    switch (mNextStage) {
    case 1:
        if (!Stage1())
            break;
        mNextStage++;
    case 2:
        if (!Stage2())
            break;
        mNextStage++;
    case 3:
        if (!Stage3())
            break;
        mNextStage++;
    case 4:
        Stage4();
        break;
    }
    Delete();
}

void DoSequencedCommand(FifoMsg* inMsg);
void DoSequencedCommand(FifoMsg* inMsg) {
    SC_SequencedCommand* cmd = (SC_SequencedCommand*)inMsg->mData;
    cmd->CallNextStage();
}

void FreeSequencedCommand(FifoMsg* inMsg);
void FreeSequencedCommand(FifoMsg* inMsg) {
    SC_SequencedCommand* cmd = (SC_SequencedCommand*)inMsg->mData;
    cmd->Delete();
}


void SC_SequencedCommand::CallNextStage() {
    bool sendAgain = false;
    FifoMsg msg;

    int isRealTime = mNextStage & 1;
    switch (mNextStage) {
    case 1:
        sendAgain = Stage1(); // RT
        break;
    case 2:
        sendAgain = Stage2(); // NRT
        break;
    case 3:
        sendAgain = Stage3(); // RT
        break;
    case 4:
        Stage4(); // NRT
        break;
    }
    mNextStage++;
    SC_AudioDriver* driver = AudioDriver(mWorld);
    if (sendAgain) {
        msg.Set(mWorld, DoSequencedCommand, nullptr, (void*)this);
        // send this to next time.
        if (isRealTime) {
            // send to NRT
            driver->SendMsgFromEngine(msg);
        } else {
            // send to RT
            driver->SendMsgToEngine(msg);
        }
    } else {
        if (isRealTime) {
            Delete();
        } else {
            // can only be freed from RT.
            msg.Set(mWorld, FreeSequencedCommand, nullptr, (void*)this);
            driver->SendMsgToEngine(msg);
        }
    }
}

void SC_SequencedCommand::Delete() {
    CallDestructor();
    World_Free(mWorld, this);
}

bool SC_SequencedCommand::Stage1() { return true; }

bool SC_SequencedCommand::Stage2() { return false; }

bool SC_SequencedCommand::Stage3() { return false; }

void SC_SequencedCommand::Stage4() {}


///////////////////////////////////////////////////////////////////////////

#include "sc_msg_iter.h"
#include <string.h>

SyncCmd::SyncCmd(World* inWorld, ReplyAddress* inReplyAddress): SC_SequencedCommand(inWorld, inReplyAddress) {}

int SyncCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mID = msg.geti();
    return kSCErr_None;
}

void SyncCmd::CallDestructor() { this->~SyncCmd(); }

bool SyncCmd::Stage2() { return true; }

bool SyncCmd::Stage3() { return true; }

void SyncCmd::Stage4() {
    small_scpacket packet;
    packet.adds("/synced");
    packet.maketags(2);
    packet.addtag(',');
    packet.addtag('i');

    packet.addi(mID);

    SendReply(&mReplyAddress, packet.data(), packet.size());
}

///////////////////////////////////////////////////////////////////////////

BufAllocCmd::BufAllocCmd(World* inWorld, ReplyAddress* inReplyAddress): SC_SequencedCommand(inWorld, inReplyAddress) {}

int BufAllocCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mBufIndex = msg.geti();
    mNumFrames = msg.geti();
    mNumChannels = msg.geti(1);

    GET_COMPLETION_MSG(msg);

    auto sampleRate = msg.getf(0);
    mSampleRate = sampleRate > 0.0 ? sampleRate : mWorld->mSampleRate;

    return kSCErr_None;
}

void BufAllocCmd::CallDestructor() { this->~BufAllocCmd(); }

bool BufAllocCmd::Stage2() {
    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);
    mFreeData = buf->data;
    SCErr err = bufAlloc(buf, mNumChannels, mNumFrames, mSampleRate);
    if (err) {
        scprintf("/b_alloc: memory allocation failed\n");
        return false;
    }
    mSndBuf = *buf;
    return true;
}

bool BufAllocCmd::Stage3() {
    SndBuf* buf = World_GetBuf(mWorld, mBufIndex);
    *buf = mSndBuf;
    mWorld->mSndBufUpdates[mBufIndex].writes++;
    SEND_COMPLETION_MSG;
    return true;
}

void BufAllocCmd::Stage4() {
    zfree(mFreeData);
    SendDoneWithIntValue("/b_alloc", mBufIndex);
}

///////////////////////////////////////////////////////////////////////////

#include "sc_msg_iter.h"
#include <string.h>

BufGenCmd::BufGenCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mData(nullptr) {}

BufGenCmd::~BufGenCmd() { World_Free(mWorld, mData); }

int BufGenCmd::Init(char* inData, int inSize) {
    mSize = inSize;
    mData = (char*)World_Alloc(mWorld, mSize);
    ReturnSCErrIfNil(mData);
    memcpy(mData, inData, mSize);

    sc_msg_iter msg(mSize, mData);
    mBufIndex = msg.geti();

    int32* genName = msg.gets4();
    if (!genName)
        return kSCErr_WrongArgType;

    mBufGen = GetBufGen(genName);
    if (!mBufGen)
        return kSCErr_BufGenNotFound;

    mMsg = msg;

    return kSCErr_None;
}

void BufGenCmd::CallDestructor() { this->~BufGenCmd(); }

bool BufGenCmd::Stage2() {
    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);

    mFreeData = buf->data;
    (*mBufGen->mBufGenFunc)(mWorld, buf, &mMsg);
    if (buf->data == mFreeData)
        mFreeData = nullptr;
    mSndBuf = *buf;
    return true;
}

bool BufGenCmd::Stage3() {
    SndBuf* buf = World_GetBuf(mWorld, mBufIndex);
    *buf = mSndBuf;
    mWorld->mSndBufUpdates[mBufIndex].writes++;
    return true;
}

void BufGenCmd::Stage4() {
    zfree(mFreeData);
    SendDoneWithIntValue("/b_gen", mBufIndex);
}


///////////////////////////////////////////////////////////////////////////


///////////////////////////////////////////////////////////////////////////


BufFreeCmd::BufFreeCmd(World* inWorld, ReplyAddress* inReplyAddress): SC_SequencedCommand(inWorld, inReplyAddress) {}

int BufFreeCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mBufIndex = msg.geti();

    GET_COMPLETION_MSG(msg);

    return kSCErr_None;
}

void BufFreeCmd::CallDestructor() { this->~BufFreeCmd(); }

bool BufFreeCmd::Stage2() {
    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);
    mFreeData = buf->data;
#ifndef NO_LIBSNDFILE
    if (buf->sndfile)
        sf_close(GETSNDFILE(buf));
#endif
    SndBuf_Init(buf);
    return true;
}

bool BufFreeCmd::Stage3() {
    SndBuf* buf = World_GetBuf(mWorld, mBufIndex);

    SndBuf_Init(buf);
    mWorld->mSndBufUpdates[mBufIndex].writes++;
    SEND_COMPLETION_MSG;

    return true;
}

void BufFreeCmd::Stage4() {
    zfree(mFreeData);
    SendDoneWithIntValue("/b_free", mBufIndex);
}

///////////////////////////////////////////////////////////////////////////

BufZeroCmd::BufZeroCmd(World* inWorld, ReplyAddress* inReplyAddress): SC_SequencedCommand(inWorld, inReplyAddress) {}

int BufZeroCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mBufIndex = msg.geti();

    GET_COMPLETION_MSG(msg);

    return kSCErr_None;
}

void BufZeroCmd::CallDestructor() { this->~BufZeroCmd(); }

bool BufZeroCmd::Stage2() {
    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);
    memset(buf->data, 0, buf->samples * sizeof(float));
    return true;
}

bool BufZeroCmd::Stage3() {
    mWorld->mSndBufUpdates[mBufIndex].writes++;
    SEND_COMPLETION_MSG;
    return true;
}

void BufZeroCmd::Stage4() { SendDoneWithIntValue("/b_zero", mBufIndex); }

///////////////////////////////////////////////////////////////////////////

BufAllocReadCmd::BufAllocReadCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mFreeData(nullptr),
    mFilename(nullptr) {}

int BufAllocReadCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mBufIndex = msg.geti();

    const char* filename = msg.gets();
    if (!filename)
        return kSCErr_WrongArgType;

    if (mWorld->mRestrictedPath) {
        mFilename = allocAndRestrictPath(mWorld, filename, mWorld->mRestrictedPath);
    } else {
        mFilename = (char*)World_Alloc(mWorld, strlen(filename) + 1);
        ReturnSCErrIfNil(mFilename);
        strcpy(mFilename, filename);
    }

    mFileOffset = msg.geti();
    mNumFrames = msg.geti();

    GET_COMPLETION_MSG(msg);

    return kSCErr_None;
}

BufAllocReadCmd::~BufAllocReadCmd() { World_Free(mWorld, mFilename); }

void BufAllocReadCmd::CallDestructor() { this->~BufAllocReadCmd(); }

bool BufAllocReadCmd::Stage2() {
#ifdef NO_LIBSNDFILE
    SendFailure(&mReplyAddress, "/b_allocRead", "scsynth compiled without libsndfile\n");
    scprintf("scsynth compiled without libsndfile\n");
    return false;
#else
    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);
    SF_INFO fileinfo;
    memset(&fileinfo, 0, sizeof(fileinfo));
    SNDFILE* sf = sndfileOpenFromCStr(mFilename, SFM_READ, &fileinfo);
    if (!sf) {
        char str[ERR_BUF_SIZE];
        snprintf(str, ERR_BUF_SIZE, "File '%s' could not be opened: %s\n", mFilename, sf_strerror(nullptr));
        SendFailureWithIntValue(&mReplyAddress, "/b_allocRead", str,
                                mBufIndex); // SendFailure(&mReplyAddress, "/b_allocRead", str);
        scprintf(str);
        return false;
    }
    if (mFileOffset < 0)
        mFileOffset = 0;
    else if (mFileOffset > fileinfo.frames)
        mFileOffset = fileinfo.frames;
    if (mNumFrames <= 0 || mNumFrames + mFileOffset > fileinfo.frames)
        mNumFrames = fileinfo.frames - mFileOffset;

    // alloc data size
    mFreeData = buf->data;
    SCErr err = bufAlloc(buf, fileinfo.channels, mNumFrames, fileinfo.samplerate);
    if (err)
        goto leave;

    sf_seek(sf, mFileOffset, SEEK_SET);
    sf_readf_float(sf, buf->data, mNumFrames);

leave:
    mSndBuf = *buf;
    sf_close(sf);

    return true;
#endif
}

bool BufAllocReadCmd::Stage3() {
    SndBuf* buf = World_GetBuf(mWorld, mBufIndex);
    *buf = mSndBuf;
    mWorld->mSndBufUpdates[mBufIndex].writes++;
    SEND_COMPLETION_MSG;

    return true;
}

void BufAllocReadCmd::Stage4() {
    zfree(mFreeData);
    SendDoneWithIntValue("/b_allocRead", mBufIndex);
}

///////////////////////////////////////////////////////////////////////////

BufReadCmd::BufReadCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mFilename(nullptr) {}

int BufReadCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mBufIndex = msg.geti();

    const char* filename = msg.gets();
    if (!filename)
        return kSCErr_WrongArgType;

    if (mWorld->mRestrictedPath) {
        mFilename = allocAndRestrictPath(mWorld, filename, mWorld->mRestrictedPath);
    } else {
        mFilename = (char*)World_Alloc(mWorld, strlen(filename) + 1);
        ReturnSCErrIfNil(mFilename);
        strcpy(mFilename, filename);
    }

    mFileOffset = msg.geti();
    mNumFrames = msg.geti(-1);
    mBufOffset = msg.geti();
    mLeaveFileOpen = msg.geti();

    GET_COMPLETION_MSG(msg);

    return kSCErr_None;
}

BufReadCmd::~BufReadCmd() { World_Free(mWorld, mFilename); }

void BufReadCmd::CallDestructor() { this->~BufReadCmd(); }

bool BufReadCmd::Stage2() {
#ifdef NO_LIBSNDFILE
    SendFailure(&mReplyAddress, "/b_read", "scsynth compiled without libsndfile\n");
    scprintf("scsynth compiled without libsndfile\n");
    return false;
#else
    SF_INFO fileinfo;
    memset(&fileinfo, 0, sizeof(fileinfo));

    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);
    int framesToEnd = buf->frames - mBufOffset;
    if (framesToEnd <= 0)
        return true;

    SNDFILE* sf = sndfileOpenFromCStr(mFilename, SFM_READ, &fileinfo);
    if (!sf) {
        char str[ERR_BUF_SIZE];
        snprintf(str, ERR_BUF_SIZE, "File '%s' could not be opened: %s\n", mFilename, sf_strerror(nullptr));
        SendFailureWithIntValue(&mReplyAddress, "/b_read", str,
                                mBufIndex); // SendFailure(&mReplyAddress, "/b_read", str);
        scprintf(str);
        return false;
    }
    if (fileinfo.channels != buf->channels) {
        char str[ERR_BUF_SIZE];
        sf_close(sf);
        snprintf(str, ERR_BUF_SIZE, "Channel mismatch. File '%s' has %d channels. Buffer has %d channels.\n", mFilename,
                 fileinfo.channels, buf->channels);
        SendFailureWithIntValue(&mReplyAddress, "/b_read", str,
                                mBufIndex); // SendFailure(&mReplyAddress, "/b_read", str);
        scprintf(str);
        return false;
    }

    if (mFileOffset < 0)
        mFileOffset = 0;
    else if (mFileOffset > fileinfo.frames)
        mFileOffset = fileinfo.frames;
    if (mNumFrames < 0 || mNumFrames + mFileOffset > fileinfo.frames)
        mNumFrames = fileinfo.frames - mFileOffset;

    if (mNumFrames > framesToEnd)
        mNumFrames = framesToEnd;

    sf_seek(sf, mFileOffset, SEEK_SET);
    if (mNumFrames > 0) {
        sf_readf_float(sf, buf->data + (mBufOffset * buf->channels), mNumFrames);
    }

    if (buf->sndfile)
        sf_close(GETSNDFILE(buf));

    if (mLeaveFileOpen) {
        buf->sndfile = sf;
    } else {
        sf_close(sf);
        buf->sndfile = nullptr;
    }

    mSampleRate = (double)fileinfo.samplerate;

    return true;
#endif
}

bool BufReadCmd::Stage3() {
    SndBuf* buf = World_GetBuf(mWorld, mBufIndex);
    buf->samplerate = mSampleRate;
    if (mLeaveFileOpen)
        buf->mask = buf->mask1 = -1;

    mWorld->mSndBufUpdates[mBufIndex].writes++;
    SEND_COMPLETION_MSG;
    return true;
}

void BufReadCmd::Stage4() { SendDoneWithIntValue("/b_read", mBufIndex); }

///////////////////////////////////////////////////////////////////////////

SC_BufReadCommand::SC_BufReadCommand(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mNumChannels(0) {}

SC_BufReadCommand::~SC_BufReadCommand() {}

void SC_BufReadCommand::InitChannels(sc_msg_iter& msg) {
    mNumChannels = 0;
    while (msg.nextTag(0) == 'i') {
        int c = msg.geti();
        if (mNumChannels <= kMaxNumChannels) {
            mChannels[mNumChannels++] = c;
        }
    }
}

bool SC_BufReadCommand::CheckChannels(int inNumChannels) {
    for (int i = 0; i < mNumChannels; ++i) {
        if (mChannels[i] >= inNumChannels) {
            return false;
        }
    }
    return true;
}

void SC_BufReadCommand::CopyChannels(float* dst, float* src, size_t srcChannels, size_t numFrames) {
    for (int ci = 0; ci < mNumChannels; ++ci) {
        int c = mChannels[ci];
        if (c >= 0 && c < srcChannels) {
            for (size_t fi = 0; fi < numFrames; ++fi) {
                dst[fi * mNumChannels + ci] = src[fi * srcChannels + c];
            }
        } else {
            for (size_t fi = 0; fi < numFrames; ++fi) {
                dst[fi * mNumChannels + ci] = 0.f;
            }
        }
    }
}

///////////////////////////////////////////////////////////////////////////

BufAllocReadChannelCmd::BufAllocReadChannelCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_BufReadCommand(inWorld, inReplyAddress),
    mFreeData(nullptr),
    mFilename(nullptr) {}

int BufAllocReadChannelCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mBufIndex = msg.geti();

    const char* filename = msg.gets();
    if (!filename)
        return kSCErr_WrongArgType;

    if (mWorld->mRestrictedPath) {
        mFilename = allocAndRestrictPath(mWorld, filename, mWorld->mRestrictedPath);
    } else {
        mFilename = (char*)World_Alloc(mWorld, strlen(filename) + 1);
        ReturnSCErrIfNil(mFilename);
        strcpy(mFilename, filename);
    }

    mFileOffset = msg.geti();
    mNumFrames = msg.geti();

    InitChannels(msg);

    GET_COMPLETION_MSG(msg);

    return kSCErr_None;
}

BufAllocReadChannelCmd::~BufAllocReadChannelCmd() { World_Free(mWorld, mFilename); }

void BufAllocReadChannelCmd::CallDestructor() { this->~BufAllocReadChannelCmd(); }

bool BufAllocReadChannelCmd::Stage2() {
#ifdef NO_LIBSNDFILE
    SendFailure(&mReplyAddress, "/b_allocReadChannel", "scsynth compiled without libsndfile\n");
    scprintf("scsynth compiled without libsndfile\n");
    return false;
#else
    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);

    SF_INFO fileinfo;
    memset(&fileinfo, 0, sizeof(fileinfo));
    SNDFILE* sf = sndfileOpenFromCStr(mFilename, SFM_READ, &fileinfo);
    if (!sf) {
        char str[ERR_BUF_SIZE];
        snprintf(str, ERR_BUF_SIZE, "File '%s' could not be opened: %s\n", mFilename, sf_strerror(nullptr));
        SendFailureWithIntValue(&mReplyAddress, "/b_allocReadChannel", str,
                                mBufIndex); // SendFailure(&mReplyAddress, "/b_allocRead", str);
        scprintf(str);
        return false;
    }
    if (mFileOffset < 0)
        mFileOffset = 0;
    else if (mFileOffset > fileinfo.frames)
        mFileOffset = fileinfo.frames;
    if (mNumFrames <= 0 || mNumFrames + mFileOffset > fileinfo.frames)
        mNumFrames = fileinfo.frames - mFileOffset;

    if (mNumChannels == 0) {
        // alloc data size
        mFreeData = buf->data;
        SCErr err = bufAlloc(buf, fileinfo.channels, mNumFrames, fileinfo.samplerate);
        if (err)
            goto leave;
        // read all channels
        sf_seek(sf, mFileOffset, SEEK_SET);
        sf_readf_float(sf, buf->data, mNumFrames);
    } else {
        // verify channel indexes
        if (!CheckChannels(fileinfo.channels)) {
            const char* str = "Channel index out of range.\n";
            SendFailureWithIntValue(&mReplyAddress, "/b_allocReadChannel", str,
                                    mBufIndex); // SendFailure(&mReplyAddress, "/b_allocRead", str);
            scprintf(str);
            sf_close(sf);
            return false;
        }
        // alloc data size
        mFreeData = buf->data;
        SCErr err = bufAlloc(buf, mNumChannels, mNumFrames, fileinfo.samplerate);
        if (err)
            goto leave;
        // alloc temp buffer
        float* data = (float*)malloc(mNumFrames * fileinfo.channels * sizeof(float));
        if (data == nullptr)
            goto leave;
        // read some channels
        sf_seek(sf, mFileOffset, SEEK_SET);
        sf_readf_float(sf, data, mNumFrames);
        CopyChannels(buf->data, data, fileinfo.channels, mNumFrames);
        // free temp buffer
        free(data);
    }

leave:
    mSndBuf = *buf;
    sf_close(sf);

    return true;
#endif
}

bool BufAllocReadChannelCmd::Stage3() {
    SndBuf* buf = World_GetBuf(mWorld, mBufIndex);
    *buf = mSndBuf;
    mWorld->mSndBufUpdates[mBufIndex].writes++;
    SEND_COMPLETION_MSG;

    return true;
}

void BufAllocReadChannelCmd::Stage4() {
    zfree(mFreeData);
    SendDoneWithIntValue("/b_allocReadChannel", mBufIndex);
}

///////////////////////////////////////////////////////////////////////////

BufReadChannelCmd::BufReadChannelCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_BufReadCommand(inWorld, inReplyAddress),
    mFilename(nullptr) {}

int BufReadChannelCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mBufIndex = msg.geti();

    const char* filename = msg.gets();
    if (!filename)
        return kSCErr_WrongArgType;

    if (mWorld->mRestrictedPath) {
        mFilename = allocAndRestrictPath(mWorld, filename, mWorld->mRestrictedPath);
    } else {
        mFilename = (char*)World_Alloc(mWorld, strlen(filename) + 1);
        ReturnSCErrIfNil(mFilename);
        strcpy(mFilename, filename);
    }

    mFileOffset = msg.geti();
    mNumFrames = msg.geti(-1);
    mBufOffset = msg.geti();
    mLeaveFileOpen = msg.geti();

    InitChannels(msg);

    GET_COMPLETION_MSG(msg);

    return kSCErr_None;
}

BufReadChannelCmd::~BufReadChannelCmd() { World_Free(mWorld, mFilename); }

void BufReadChannelCmd::CallDestructor() { this->~BufReadChannelCmd(); }

bool BufReadChannelCmd::Stage2() {
#ifdef NO_LIBSNDFILE
    SendFailure(&mReplyAddress, "/b_readChannel", "scsynth compiled without libsndfile\n");
    scprintf("scsynth compiled without libsndfile\n");
    return false;
#else
    SF_INFO fileinfo;
    memset(&fileinfo, 0, sizeof(fileinfo));

    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);
    int framesToEnd = buf->frames - mBufOffset;
    if (framesToEnd <= 0)
        return true;

    SNDFILE* sf = sndfileOpenFromCStr(mFilename, SFM_READ, &fileinfo);
    if (!sf) {
        char str[ERR_BUF_SIZE];
        snprintf(str, ERR_BUF_SIZE, "File '%s' could not be opened: %s\n", mFilename, sf_strerror(nullptr));
        SendFailureWithIntValue(&mReplyAddress, "/b_readChannel", str,
                                mBufIndex); // SendFailure(&mReplyAddress, "/b_read", str);
        scprintf(str);
        return false;
    }

    if (mNumChannels > 0) {
        // verify channel indexes
        if (!(CheckChannels(fileinfo.channels))) { // nescivi:  && CheckChannels(buf->channels) (should not check here
                                                   // for buf->channels)
            const char* str = "Channel index out of range.\n";
            SendFailureWithIntValue(&mReplyAddress, "/b_readChannel", str,
                                    mBufIndex); // SendFailure(&mReplyAddress, "/b_allocRead", str);
            scprintf(str);
            sf_close(sf);
            return false;
        }
        // nescivi: this also seems out of place: we want to read from a file with more channels than are in the buffer
        // 	} else if (fileinfo.channels != buf->channels) {
        // 		char str[256];
        // 		sf_close(sf);
        // 		sprintf(str, "Channel mismatch. File '%s' has %d channels. Buffer has %d channels.\n",
        // 				mFilename, fileinfo.channels, buf->channels);
        // 		SendFailure(&mReplyAddress, "/b_read", str);
        // 		scprintf(str);
        // 		return false;
    }

    if (mFileOffset < 0)
        mFileOffset = 0;
    else if (mFileOffset > fileinfo.frames)
        mFileOffset = fileinfo.frames;
    if (mNumFrames < 0 || mNumFrames + mFileOffset > fileinfo.frames)
        mNumFrames = fileinfo.frames - mFileOffset;
    if (mNumFrames > framesToEnd)
        mNumFrames = framesToEnd;

    sf_seek(sf, mFileOffset, SEEK_SET);
    if (mNumFrames > 0) {
        if (mNumChannels == 0) {
            // read all channels
            sf_readf_float(sf, buf->data + (mBufOffset * buf->channels), mNumFrames);
        } else {
            // alloc temp buffer
            float* data = (float*)malloc(mNumFrames * fileinfo.channels * sizeof(float));
            if (data == nullptr)
                goto leave;
            // read some channels
            sf_seek(sf, mFileOffset, SEEK_SET);
            sf_readf_float(sf, data, mNumFrames);
            CopyChannels(buf->data + (mBufOffset * mNumChannels), data, fileinfo.channels, mNumFrames);
            // free temp buffer
            free(data);
        }
    }

leave:
    if (buf->sndfile)
        sf_close(GETSNDFILE(buf));

    if (mLeaveFileOpen) {
        buf->sndfile = sf;
    } else {
        sf_close(sf);
        buf->sndfile = nullptr;
    }

    mSampleRate = (double)fileinfo.samplerate;

    return true;
#endif
}

bool BufReadChannelCmd::Stage3() {
    SndBuf* buf = World_GetBuf(mWorld, mBufIndex);
    buf->samplerate = mSampleRate;
    if (mLeaveFileOpen)
        buf->mask = buf->mask1 = -1;

    mWorld->mSndBufUpdates[mBufIndex].writes++;
    SEND_COMPLETION_MSG;
    return true;
}

void BufReadChannelCmd::Stage4() { SendDoneWithIntValue("/b_readChannel", mBufIndex); }

///////////////////////////////////////////////////////////////////////////

BufWriteCmd::BufWriteCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mFilename(nullptr) {}

#ifdef NO_LIBSNDFILE
struct SF_INFO {};
#endif

int BufWriteCmd::Init(char* inData, int inSize) {
#ifdef NO_LIBSNDFILE
    SendFailure(&mReplyAddress, "/b_write", "scsynth compiled without libsndfile\n");
    scprintf("scsynth compiled without libsndfile\n");
    return false;
#else
    sc_msg_iter msg(inSize, inData);
    mBufIndex = msg.geti();

    const char* filename = msg.gets();
    if (!filename)
        return kSCErr_WrongArgType;

    if (mWorld->mRestrictedPath) {
        mFilename = allocAndRestrictPath(mWorld, filename, mWorld->mRestrictedPath);
    } else {
        mFilename = (char*)World_Alloc(mWorld, strlen(filename) + 1);
        ReturnSCErrIfNil(mFilename);
        strcpy(mFilename, filename);
    }

    const char* headerFormatString = msg.gets("aiff");
    const char* sampleFormatString = msg.gets("int16");

    mNumFrames = msg.geti(-1);
    mBufOffset = msg.geti();
    mLeaveFileOpen = msg.geti();

    GET_COMPLETION_MSG(msg);

    memset(&mFileInfo, 0, sizeof(mFileInfo));
    return sndfileFormatInfoFromStrings(&mFileInfo, headerFormatString, sampleFormatString);
#endif
}

BufWriteCmd::~BufWriteCmd() { World_Free(mWorld, mFilename); }

void BufWriteCmd::CallDestructor() { this->~BufWriteCmd(); }

bool BufWriteCmd::Stage2() {
#ifdef NO_LIBSNDFILE
    return false;
#else
    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);
    int framesToEnd = buf->frames - mBufOffset;
    if (framesToEnd < 0)
        framesToEnd = 0;
    mFileInfo.samplerate = (int)buf->samplerate;
    mFileInfo.channels = buf->channels;

    SNDFILE* sf = sndfileOpenFromCStr(mFilename, SFM_WRITE, &mFileInfo);
    if (!sf) {
        char str[ERR_BUF_SIZE];
        snprintf(str, ERR_BUF_SIZE, "File '%s' could not be opened: %s\n", mFilename, sf_strerror(nullptr));
        SendFailureWithIntValue(&mReplyAddress, "/b_write", str,
                                mBufIndex); // SendFailure(&mReplyAddress, "/b_write", str);
        scprintf(str);
        return false;
    }

    if (mNumFrames < 0 || mNumFrames > buf->frames)
        mNumFrames = buf->frames;

    if (mNumFrames > framesToEnd)
        mNumFrames = framesToEnd;

    sf_command(sf, SFC_SET_CLIPPING, nullptr,
               SF_TRUE); // choose clipping rather than wraparound for integer-format files

    if (mNumFrames > 0) {
        sf_writef_float(sf, buf->data + (mBufOffset * buf->channels), mNumFrames);
    }

    if (buf->sndfile)
        sf_close(GETSNDFILE(buf));

    if (mLeaveFileOpen) {
        buf->sndfile = sf;
    } else {
        sf_close(sf);
        buf->sndfile = nullptr;
    }

    return true;
#endif
}

bool BufWriteCmd::Stage3() {
    SEND_COMPLETION_MSG;
    return true;
}

void BufWriteCmd::Stage4() { SendDoneWithIntValue("/b_write", mBufIndex); }

///////////////////////////////////////////////////////////////////////////

BufCloseCmd::BufCloseCmd(World* inWorld, ReplyAddress* inReplyAddress): SC_SequencedCommand(inWorld, inReplyAddress) {}

int BufCloseCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mBufIndex = msg.geti();

    GET_COMPLETION_MSG(msg);

    return kSCErr_None;
}

void BufCloseCmd::CallDestructor() { this->~BufCloseCmd(); }

bool BufCloseCmd::Stage2() {
#ifdef NO_LIBSNDFILE
    SendFailure(&mReplyAddress, "/b_close", "scsynth compiled without libsndfile\n");
    scprintf("scsynth compiled without libsndfile\n");
    return false;
#else
    SndBuf* buf = World_GetNRTBuf(mWorld, mBufIndex);
    if (buf->sndfile) {
        sf_close(GETSNDFILE(buf));
        buf->sndfile = nullptr;
    }
    return true;
#endif
}

bool BufCloseCmd::Stage3() {
    SEND_COMPLETION_MSG;
    return true;
}

void BufCloseCmd::Stage4() { SendDoneWithIntValue("/b_close", mBufIndex); }

///////////////////////////////////////////////////////////////////////////

AudioQuitCmd::AudioQuitCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress) {}

void AudioQuitCmd::CallDestructor() { this->~AudioQuitCmd(); }

bool AudioQuitCmd::Stage2() {
    mWorld->hw->mTerminating = true;
    return true;
}

bool AudioQuitCmd::Stage3() {
#if SC_AUDIO_API == SC_AUDIO_API_AUDIOUNITS
    SendFailure(&mReplyAddress, "/quit", "not allowed in AU host\n");
    scprintf("/quit : quit not allowed in AU host\n");
    return false;
#else
    if (mWorld->hw->mShmem)
        mWorld->hw->mShmem->disconnect();
    return true;
#endif
}

void AudioQuitCmd::Stage4() {
    SendDone("/quit");
    mWorld->hw->mQuitProgram->post();
}

///////////////////////////////////////////////////////////////////////////

AudioStatusCmd::AudioStatusCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress) {}

void AudioStatusCmd::CallDestructor() { this->~AudioStatusCmd(); }

bool AudioStatusCmd::Stage2() {
    // we stop replying to status requests after receiving /quit
    if (mWorld->hw->mTerminating == true)
        return false;

    small_scpacket packet;
    packet.adds("/status.reply");
    packet.maketags(10);
    packet.addtag(',');
    packet.addtag('i');
    packet.addtag('i');
    packet.addtag('i');
    packet.addtag('i');
    packet.addtag('i');
    packet.addtag('f');
    packet.addtag('f');
    packet.addtag('d');
    packet.addtag('d');

    packet.addi(1); // audio is always active now.
    packet.addi(mWorld->mNumUnits);
    packet.addi(mWorld->mNumGraphs);
    packet.addi(mWorld->mNumGroups);
    packet.addi(mWorld->hw->mGraphDefLib->NumItems());

    SC_AudioDriver* driver = mWorld->hw->mAudioDriver;
    packet.addf(driver->GetAvgCPU());
    packet.addf(driver->GetPeakCPU());
    packet.addd(driver->GetSampleRate());
    packet.addd(driver->GetActualSampleRate());

    SendReply(&mReplyAddress, packet.data(), packet.size());

    return false;
}

///////////////////////////////////////////////////////////////////////////

RTMemStatusCmd::RTMemStatusCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress) {}

void RTMemStatusCmd::CallDestructor() { this->~RTMemStatusCmd(); }

bool RTMemStatusCmd::Stage2() {
    // we stop replying to status requests after receiving /quit
    if (mWorld->hw->mTerminating == true)
        return false;

    small_scpacket packet;
    packet.adds("/rtMemoryStatus.reply");
    packet.maketags(3);
    packet.addtag(',');
    packet.addtag('i');
    packet.addtag('i');

    packet.addi(World_TotalFree(mWorld));
    packet.addi(World_LargestFreeChunk(mWorld));

    SendReply(&mReplyAddress, packet.data(), packet.size());

    return false;
}

///////////////////////////////////////////////////////////////////////////

NotifyCmd::NotifyCmd(World* inWorld, ReplyAddress* inReplyAddress): SC_SequencedCommand(inWorld, inReplyAddress) {}

int NotifyCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);
    mOnOff = msg.geti();
    mID = msg.geti(-1);

    return kSCErr_None;
}

void NotifyCmd::CallDestructor() { this->~NotifyCmd(); }

/** \brief Attempts to find and remove the requested \c id from \c availableIDs.
 *
 * If \c id is -1 or not in \c availableIDs, returns the first element in
 * \c availableIDs. Otherwise, returns \c id.
 */
int popAvailableClientID(int const id, ClientIDs& availableIDs) {
    int clientID = -1;
    if (id == -1) {
        // no requested clientID
        clientID = availableIDs.front(); // pop an ID
        availableIDs.pop_front();
    } else {
        // user ID requested
        auto it = std::find(availableIDs.begin(), availableIDs.end(), id);
        if (it != availableIDs.end()) { // return the requested ID if available
            clientID = id;
            availableIDs.erase(it);
        } else {
            // otherwise return the first free one
            clientID = availableIDs.front();
            availableIDs.pop_front();
        }
    }

    return clientID;
}

bool NotifyCmd::Stage2() {
    HiddenWorld* hw = mWorld->hw;

    if (mOnOff) {
        for (auto addr : *hw->mUsers) {
            if (mReplyAddress == addr) {
                // already in table - don't fail though..
                SendFailureWithIntValue(&mReplyAddress, "/notify", "notify: already registered\n",
                                        hw->mClientIDdict->at(mReplyAddress));
                scprintf("/notify : already registered\n");
                return false;
            }
        }

        if (hw->mUsers->size() >= hw->mMaxUsers) {
            SendFailure(&mReplyAddress, "/notify", "too many users\n");
            scprintf("too many users\n");
            return false;
        }

        int const clientID = popAvailableClientID(mID, *hw->mAvailableClientIDs);

        hw->mClientIDdict->insert(std::make_pair(mReplyAddress, clientID));
        hw->mUsers->insert(mReplyAddress);
        SendDoneWithVarArgs(&mReplyAddress, "/notify", "ii", clientID, (int)hw->mMaxUsers);

    } else {
        auto const it = std::find(hw->mUsers->begin(), hw->mUsers->end(), mReplyAddress);
        if (it != hw->mUsers->end()) {
            // remove from list
            hw->mAvailableClientIDs->push_back(hw->mClientIDdict->at(mReplyAddress)); // push the freed ID
            hw->mClientIDdict->erase(mReplyAddress);
            hw->mUsers->erase(it);
            SendDone("/notify");
            return false;
        }

        SendFailure(&mReplyAddress, "/notify", "not registered\n");
        scprintf("not registered\n");
    }
    return false;
}

///////////////////////////////////////////////////////////////////////////

SendFailureCmd::SendFailureCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mCmdName(nullptr),
    mErrString(nullptr) {}

SendFailureCmd::~SendFailureCmd() {
    World_Free(mWorld, mCmdName);
    World_Free(mWorld, mErrString);
}


void SendFailureCmd::InitSendFailureCmd(const char* inCmdName, const char* inErrString) {
    mCmdName = (char*)World_Alloc(mWorld, strlen(inCmdName) + 1);
    ThrowIfNil(mCmdName);
    strcpy(mCmdName, inCmdName);

    mErrString = (char*)World_Alloc(mWorld, strlen(inErrString) + 1);
    ThrowIfNil(mErrString);
    strcpy(mErrString, inErrString);
}

void SendFailureCmd::CallDestructor() { this->~SendFailureCmd(); }

bool SendFailureCmd::Stage2() {
    SendFailure(&mReplyAddress, mCmdName, mErrString);
    return false;
}

///////////////////////////////////////////////////////////////////////////

RecvSynthDefCmd::RecvSynthDefCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mBuffer(nullptr) {}

int RecvSynthDefCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);

    int size = msg.getbsize();
    if (!size)
        throw kSCErr_WrongArgType;

    mBuffer = (char*)World_Alloc(mWorld, size);
    ReturnSCErrIfNil(mBuffer);
    msg.getb(mBuffer, size);

    GET_COMPLETION_MSG(msg);

    mDefs = nullptr;
    return kSCErr_None;
}

RecvSynthDefCmd::~RecvSynthDefCmd() { World_Free(mWorld, mBuffer); }

void RecvSynthDefCmd::CallDestructor() { this->~RecvSynthDefCmd(); }

bool RecvSynthDefCmd::Stage2() {
    mDefs = GraphDef_Recv(mWorld, mBuffer, mDefs);

    return true;
}

bool RecvSynthDefCmd::Stage3() {
    GraphDef_Define(mWorld, mDefs);
    SEND_COMPLETION_MSG;
    return true;
}

void RecvSynthDefCmd::Stage4() { SendDone("/d_recv"); }

///////////////////////////////////////////////////////////////////////////

LoadSynthDefCmd::LoadSynthDefCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mFilename(nullptr) {}

int LoadSynthDefCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);

    const char* filename = msg.gets();
    if (!filename)
        return kSCErr_WrongArgType;

    if (mWorld->mRestrictedPath) {
        mFilename = allocAndRestrictPath(mWorld, filename, mWorld->mRestrictedPath);
    } else {
        mFilename = (char*)World_Alloc(mWorld, strlen(filename) + 1);
        ReturnSCErrIfNil(mFilename);
        strcpy(mFilename, filename);
    }

    GET_COMPLETION_MSG(msg);

    mDefs = nullptr;
    return kSCErr_None;
}

LoadSynthDefCmd::~LoadSynthDefCmd() { World_Free(mWorld, mFilename); }

void LoadSynthDefCmd::CallDestructor() { this->~LoadSynthDefCmd(); }

bool LoadSynthDefCmd::Stage2() {
    char* fname = mFilename;
    mDefs = GraphDef_LoadGlob(mWorld, fname, mDefs);
    if (mDefs) {
        return true;
    }
    {
        char str[ERR_BUF_SIZE];
        snprintf(str, ERR_BUF_SIZE, "File '%s' could not be opened\n", mFilename);
        SendFailure(&mReplyAddress, "/d_load", mFilename);
        scprintf(str);
        return false;
    }
}

bool LoadSynthDefCmd::Stage3() {
    GraphDef_Define(mWorld, mDefs);
    SEND_COMPLETION_MSG;
    return true;
}

void LoadSynthDefCmd::Stage4() { SendDone("/d_load"); }

///////////////////////////////////////////////////////////////////////////

LoadSynthDefDirCmd::LoadSynthDefDirCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mFilename(nullptr) {}

int LoadSynthDefDirCmd::Init(char* inData, int inSize) {
    sc_msg_iter msg(inSize, inData);

    const char* filename = msg.gets();
    if (!filename)
        return kSCErr_WrongArgType;

    if (mWorld->mRestrictedPath) {
        mFilename = allocAndRestrictPath(mWorld, filename, mWorld->mRestrictedPath);
    } else {
        mFilename = (char*)World_Alloc(mWorld, strlen(filename) + 1);
        ReturnSCErrIfNil(mFilename);
        strcpy(mFilename, filename);
    }

    GET_COMPLETION_MSG(msg);

    mDefs = nullptr;
    return kSCErr_None;
}

LoadSynthDefDirCmd::~LoadSynthDefDirCmd() { World_Free(mWorld, mFilename); }

void LoadSynthDefDirCmd::CallDestructor() { this->~LoadSynthDefDirCmd(); }

bool LoadSynthDefDirCmd::Stage2() {
    if (!std::filesystem::exists(mFilename)) {
        char str[ERR_BUF_SIZE];
        snprintf(str, ERR_BUF_SIZE, "Could not load synthdefs. Directory '%s' does not exist\n", mFilename);
        SendFailure(&mReplyAddress, "/d_loadDir", mFilename);
        scprintf(str);
        return false;
    }
    mDefs = GraphDef_LoadDir(mWorld, mFilename, mDefs);
    if (mDefs) {
        return true;
    }
    {
        char str[ERR_BUF_SIZE];
        snprintf(str, ERR_BUF_SIZE, "No synthdefs found in directory: '%s'\n", mFilename);
        SendFailure(&mReplyAddress, "/d_loadDir", mFilename);
        scprintf(str);
        return false;
    }
}

bool LoadSynthDefDirCmd::Stage3() {
    GraphDef_Define(mWorld, mDefs);
    SEND_COMPLETION_MSG;
    return true;
}

void LoadSynthDefDirCmd::Stage4() { SendDone("/d_loadDir"); }

///////////////////////////////////////////////////////////////////////////

SendReplyCmd::SendReplyCmd(World* inWorld, ReplyAddress* inReplyAddress):
    SC_SequencedCommand(inWorld, inReplyAddress) {}

int SendReplyCmd::Init(char* inData, int inSize) {
    mMsgSize = inSize;
    mMsgData = (char*)World_Alloc(mWorld, mMsgSize);
    ReturnSCErrIfNil(mMsgData);
    memcpy(mMsgData, inData, inSize);
    return kSCErr_None;
}

void SendReplyCmd::CallDestructor() { this->~SendReplyCmd(); }

bool SendReplyCmd::Stage2() {
    SendReply(&mReplyAddress, mMsgData, mMsgSize);
    return false;
}

///////////////////////////////////////////////////////////////////////////

int PerformAsynchronousCommand(
    World* inWorld, void* replyAddr, const char* cmdName, void* cmdData,
    AsyncStageFn stage2, // stage2 is non real time
    AsyncStageFn stage3, // stage3 is real time - completion msg performed if stage3 returns true
    AsyncStageFn stage4, // stage4 is non real time - sends done if stage4 returns true
    AsyncFreeFn cleanup, int completionMsgSize, void* completionMsgData) {
    void* space = World_Alloc(inWorld, sizeof(AsyncPlugInCmd));
    ReturnSCErrIfNil(space);
    AsyncPlugInCmd* cmd = new (space) AsyncPlugInCmd(inWorld, (ReplyAddress*)replyAddr, cmdName, cmdData, stage2,
                                                     stage3, stage4, cleanup, completionMsgSize, completionMsgData);
    if (!cmd)
        return kSCErr_Failed;
    if (inWorld->mRealTime)
        cmd->CallNextStage();
    else
        cmd->CallEveryStage();
    return kSCErr_None;
}

///////////////////////////////////////////////////////////////////////////

AsyncPlugInCmd::AsyncPlugInCmd(
    World* inWorld, ReplyAddress* inReplyAddress, const char* cmdName, void* cmdData,
    AsyncStageFn stage2, // stage2 is non real time
    AsyncStageFn stage3, // stage3 is real time - completion msg performed if stage3 returns true
    AsyncStageFn stage4, // stage4 is non real time - sends done if stage4 returns true
    AsyncFreeFn cleanup, // cleanup is called in real time
    int completionMsgSize, void* completionMsgData):
    SC_SequencedCommand(inWorld, inReplyAddress),
    mCmdName(cmdName),
    mCmdData(cmdData),
    mStage2(stage2),
    mStage3(stage3),
    mStage4(stage4),
    mCleanup(cleanup) {
    if (completionMsgSize && completionMsgData) {
        mMsgSize = completionMsgSize;
        mMsgData = (char*)World_Alloc(mWorld, mMsgSize);
        ThrowIfNil(mMsgData);
        memcpy(mMsgData, completionMsgData, mMsgSize);
    }
}

AsyncPlugInCmd::~AsyncPlugInCmd() { (mCleanup)(mWorld, mCmdData); }

void AsyncPlugInCmd::CallDestructor() { this->~AsyncPlugInCmd(); }

bool AsyncPlugInCmd::Stage2() {
    bool result = !mStage2 || (mStage2)(mWorld, mCmdData);
    return result;
}

bool AsyncPlugInCmd::Stage3() {
    bool result = !mStage3 || (mStage3)(mWorld, mCmdData);
    if (result)
        SEND_COMPLETION_MSG;
    return result;
}

void AsyncPlugInCmd::Stage4() {
    bool result = !mStage4 || (mStage4)(mWorld, mCmdData);
    if (result && mCmdName && mReplyAddress.mReplyFunc != null_reply_func)
        SendDone((char*)mCmdName);
}
