//  interface for supercollider plugins
//  Copyright (C) 2009, 2010 Tim Blechmann
//
//  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; see the file COPYING.  If not, write to
//  the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
//  Boston, MA 02111-1307, USA.

#include <cstdarg>
#include <random>

#include "SC_Win32Utils.h"

#include "SC_SndFileHelpers.hpp"

#include "sc_plugin_interface.hpp"
#include "sc_ugen_factory.hpp"
#include "sc_synth.hpp"

#include "nova-simd/simd_memory.hpp"

#include "../server/server_args.hpp"
#include "../server/memory_pool.hpp"
#include "../server/server.hpp"
#include "malloc_aligned.hpp"
#include "../utilities/sized_array.hpp"

#include "SC_Prototypes.h"
#include "SC_Unit.h"
#include "clz.h"
#include "SC_fftlib.hpp"
#include "SC_Lock.h"
#include "../../common/Samp.hpp"
#include "../../common/SC_SndFileHelpers.hpp"

#include <boost/math/constants/constants.hpp>

// undefine the shadowed scfft functions
#undef scfft_create
#undef scfft_dofft
#undef scfft_doifft
#undef scfft_destroy

namespace nova {

spin_lock log_guard; // needs to be acquired for logging from the helper threads!

namespace {

spin_lock rt_pool_guard;

inline Node* as_Node(server_node* node) {
    if (node == nullptr)
        return nullptr;

    // hack!!! we only assume that the 32bit integer mID member can be accessed via Node
    if (node->is_synth()) {
        sc_synth* s = static_cast<sc_synth*>(node);
        return &s->mNode;
    } else {
        void* nodePointer = &node->node_id;
        return (Node*)nodePointer;
    }
}

void pause_node(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_pause_node(node);
}

void free_node(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);
}

void free_node_and_preceding(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of preceding nodes\n");
        return;
    }

    server_node* preceding = node->previous_node();
    if (preceding)
        sc_factory->add_done_node(preceding);
}

void free_node_and_pause_preceding(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of preceding nodes\n");
        return;
    }

    server_node* preceding = node->previous_node();
    if (preceding)
        sc_factory->add_pause_node(preceding);
}

void free_node_and_preceding_children(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of preceding nodes");
        return;
    }

    server_node* preceding = node->previous_node();
    if (!preceding)
        return;
    if (preceding->is_synth())
        sc_factory->add_done_node(preceding);
    else {
        abstract_group* preceding_group = static_cast<abstract_group*>(preceding);
        sc_factory->add_freeAll_node(preceding_group);
    }
}


void free_node_and_preceding_deep(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        log("parallel groups have no notion of preceding nodes\n");
        return;
    }

    server_node* preceding = node->previous_node();
    if (!preceding)
        return;
    if (preceding->is_synth())
        sc_factory->add_done_node(preceding);
    else {
        abstract_group* preceding_group = static_cast<abstract_group*>(preceding);
        sc_factory->add_freeDeep_node(preceding_group);
    }
}

void free_node_and_all_preceding(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of preceding nodes\n");
        return;
    }

    for (;;) {
        node = node->previous_node();
        if (node)
            sc_factory->add_done_node(node);
        else
            return;
    }
}

void free_node_and_following(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of following nodes\n");
        return;
    }

    server_node* next = node->next_node();
    if (next)
        sc_factory->add_done_node(next);
}

void free_node_and_pause_following(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of following nodes\n");
        return;
    }

    server_node* next = node->next_node();
    if (next)
        sc_factory->add_pause_node(next);
}

void free_node_and_resume_following(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of following nodes\n");
        return;
    }

    server_node* next = node->next_node();
    if (next)
        sc_factory->add_resume_node(next);
}

void free_node_and_following_children(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of following nodes\n");
        return;
    }

    server_node* following = node->previous_node();
    if (!following)
        return;
    if (following->is_synth())
        sc_factory->add_done_node(following);
    else {
        abstract_group* following_group = static_cast<abstract_group*>(following);
        sc_factory->add_freeAll_node(following_group);
    }
}

void free_node_and_following_deep(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of following nodes\n");
        return;
    }

    server_node* following = node->previous_node();
    if (!following)
        return;
    if (following->is_synth())
        sc_factory->add_done_node(following);
    else {
        abstract_group* following_group = static_cast<abstract_group*>(following);
        sc_factory->add_freeDeep_node(following_group);
    }
}

void free_node_and_all_following(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    sc_factory->add_done_node(node);

    if (node->get_parent()->is_parallel()) {
        spin_lock::scoped_lock lock(log_guard);
        log("parallel groups have no notion of following nodes\n");
        return;
    }

    for (;;) {
        node = node->previous_node();
        if (node)
            sc_factory->add_done_node(node);
        else
            return;
    }
}

void free_group_members(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    abstract_group* group = const_cast<abstract_group*>(node->get_parent());

    sc_factory->add_freeAll_node(group);
}

void free_parent_group(Unit* unit) {
    server_node* node = static_cast<sc_synth*>(unit->mParent);
    abstract_group* group = const_cast<abstract_group*>(node->get_parent());
    sc_factory->add_done_node(group);
}

SCBool get_scope_buffer(World* inWorld, int32 index, int32 channels, int32 maxFrames, ScopeBufferHnd* hnd) {
    scope_buffer_writer writer = instance->get_scope_buffer_writer(index, channels, maxFrames);

    if (writer.valid()) {
        hnd->internalData = writer.buffer;
        hnd->data = writer.data();
        hnd->channels = channels;
        hnd->maxFrames = maxFrames;
        return true;
    } else {
        hnd->internalData = nullptr;
        return false;
    }
}

void push_scope_buffer(World* inWorld, ScopeBufferHnd* hnd, int frames) {
    scope_buffer_writer writer(reinterpret_cast<scope_buffer*>(hnd->internalData));
    writer.push(frames);
    hnd->data = writer.data();
}

void release_scope_buffer(World* inWorld, ScopeBufferHnd* hnd) {
    scope_buffer_writer writer(reinterpret_cast<scope_buffer*>(hnd->internalData));
    instance->release_scope_buffer_writer(writer);
}

} /* namespace */
} /* namespace nova */

extern "C" {

SCBool define_unit(const char* inUnitClassName, size_t inAllocSize, UnitCtorFunc inCtor, UnitDtorFunc inDtor,
                   uint32 inFlags) {
    try {
        nova::sc_factory->register_ugen(inUnitClassName, inAllocSize, inCtor, inDtor, inFlags);
        return true;
    } catch (...) { return false; }
}

SCBool define_bufgen(const char* name, BufGenFunc func) {
    try {
        nova::sc_factory->register_bufgen(name, func);
        return true;
    } catch (...) { return false; }
}

SCBool define_unitcmd(const char* unitClassName, const char* cmdName, UnitCmdFunc inFunc) {
    return nova::sc_factory->register_ugen_command_function(unitClassName, cmdName, inFunc);
}

SCBool define_plugincmd(const char* name, PlugInCmdFunc func, void* user_data) {
    return nova::sc_factory->register_cmd_plugin(name, func, user_data);
}

void* rt_alloc(World* dummy, size_t size) {
    nova::spin_lock::scoped_lock lock(nova::rt_pool_guard);
    if (size)
        return nova::rt_pool.malloc(size);
    else
        return nullptr;
}

void* rt_realloc(World* dummy, void* ptr, size_t size) {
    nova::spin_lock::scoped_lock lock(nova::rt_pool_guard);
    return nova::rt_pool.realloc(ptr, size);
}

void rt_free(World* dummy, void* ptr) {
    nova::spin_lock::scoped_lock lock(nova::rt_pool_guard);
    if (ptr)
        nova::rt_pool.free(ptr);
}

void* nrt_alloc(size_t size) { return malloc(size); }

void* nrt_realloc(void* ptr, size_t size) { return realloc(ptr, size); }

void nrt_free(void* ptr) { free(ptr); }

void* nrt_alloc_2(World* dummy, size_t size) { return malloc(size); }

void* nrt_realloc_2(World* dummy, void* ptr, size_t size) { return realloc(ptr, size); }

void nrt_free_2(World* dummy, void* ptr) { free(ptr); }


void clear_outputs(Unit* unit, int samples) {
    const uint32_t outputs = unit->mNumOutputs;

    if ((samples & 63) == 0) {
        const uint32_t loops = samples / 64;

        for (int i = 0; i != outputs; ++i) {
            float* buffer = unit->mOutBuf[i];
            for (int loop = 0; loop != loops; ++loop) {
                nova::zerovec_simd<64>(buffer + loop * 64);
            }
        }
        return;
    }

    if ((samples & 15) == 0) {
        for (int i = 0; i != outputs; ++i)
            nova::zerovec_simd(unit->mOutBuf[i], samples);
        return;
    }

    for (int i = 0; i != outputs; ++i)
        nova::zerovec(unit->mOutBuf[i], samples);
}

void node_end(struct Node* node) {
    nova::server_node* s = nova::instance->find_node(node->mID);
    nova::sc_factory->add_done_node(s);
}

void node_set_run(struct Node* node, int run) {
    using namespace nova;
    server_node* s = instance->find_node(node->mID);

    if (run == 0)
        sc_factory->add_pause_node(s);
    else
        sc_factory->add_resume_node(s);
}


int print(const char* fmt, ...) {
    va_list vargs;
    va_start(vargs, fmt);

    nova::log_guard.lock();
    bool status = nova::log_printf(fmt, vargs);
    nova::log_guard.unlock();

    va_end(vargs);
    return (status == true) ? 0 : -1;
}

/* todo: we need to implement most of the done actions */
void done_action(int done_action, struct Unit* unit) {
    using namespace nova;
    switch (done_action) {
    case 0:
        // do nothing when the UGen is finished
        return;

    case 1:
        // pause the enclosing synth, but do not free it
        nova::pause_node(unit);
        return;
    case 2:
        // free the enclosing synth
        nova::free_node(unit);
        return;

    case 3:
        // free both this synth and the preceding node
        nova::free_node_and_preceding(unit);
        return;

    case 4:
        // free both this synth and the following node
        nova::free_node_and_following(unit);
        return;

    case 5:
        // free this synth; if the preceding node is a group then do g_freeAll on it, else free it
        nova::free_node_and_preceding_children(unit);
        return;

    case 6:
        nova::free_node_and_following_children(unit);
        // free this synth; if the following node is a group then do g_freeAll on it, else free it
        return;

    case 7:
        // free this synth and all preceding nodes in this group
        nova::free_node_and_all_preceding(unit);
        return;

    case 8:
        // free this synth and all following nodes in this group
        nova::free_node_and_all_following(unit);
        return;

    case 9:
        // free this synth and pause the preceding node
        nova::free_node_and_pause_preceding(unit);
        return;

    case 10:
        // free this synth and pause the following node
        nova::free_node_and_pause_following(unit);
        return;

    case 11:
        // free this synth and if the preceding node is a group then do g_deepFree on it, else free it
        nova::free_node_and_preceding_deep(unit);
        return;

    case 12:
        // free this synth and if the following node is a group then do g_deepFree on it, else free it
        nova::free_node_and_following_deep(unit);
        return;

    case 13:
        // free this synth and all other nodes in this group (before and after)
        nova::free_group_members(unit);
        return;

    case 14:
        // free the enclosing group and all nodes within it (including this synth)
        nova::free_parent_group(unit);
        return;

    case 15:
        // free this synth and resume the following node
        nova::free_node_and_resume_following(unit);
        return;

    default:
        return;
    }
}

int buf_alloc(SndBuf* buf, int channels, int frames, double samplerate) {
    try {
        nova::sc_factory->allocate_buffer(buf, channels, frames, samplerate);
        return kSCErr_None;
    } catch (std::exception const& e) {
        std::cout << e.what() << std::endl;
        return kSCErr_Failed;
    }
}

void send_trigger(Node* unit, int trigger_id, float value) {
    nova::instance->send_trigger(unit->mID, trigger_id, value);
}

void world_lock(World* world) { reinterpret_cast<SC_Lock*>(world->mNRTLock)->lock(); }

void world_unlock(World* world) { reinterpret_cast<SC_Lock*>(world->mNRTLock)->unlock(); }

Node* get_node(World* world, int id) {
    nova::server_node* node = nova::instance->find_node(id);
    return nova::as_Node(node);
}

void send_node_reply(Node* node, int reply_id, const char* command_name, int argument_count, const float* values) {
    if (!nova::sc_factory->world.mRealTime)
        return;

    nova::instance->send_node_reply(node->mID, reply_id, command_name, argument_count, values);
}

SCErr do_asynchronous_command(
    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, const void* completionMsgData) {
    nova::instance->do_asynchronous_command(inWorld, replyAddr, cmdName, cmdData, stage2, stage3, stage4, cleanup,
                                            completionMsgSize, completionMsgData);
    return 0;
}

SCBool send_message_from_RT(World* world, struct FifoMsg* msg) {
    nova::instance->send_message_from_RT(world, *msg);
    return true;
}

SCBool send_message_to_RT(World* world, struct FifoMsg* msg) {
    nova::instance->send_message_to_RT(world, *msg);
    return true;
}

} /* extern "C" */

namespace nova {


inline void initialize_rate(Rate& rate, double sample_rate, int blocksize) {
    rate.mSampleRate = sample_rate;
    rate.mSampleDur = 1. / sample_rate;
    rate.mRadiansPerSample = 2 * boost::math::constants::pi<double>() / sample_rate;

    rate.mBufLength = blocksize;
    rate.mBufDuration = blocksize / sample_rate;
    rate.mBufRate = sample_rate / blocksize;
    rate.mSlopeFactor = 1. / blocksize;

    div_t div_result = std::div(blocksize, 3);
    rate.mFilterLoops = div_result.quot;
    rate.mFilterRemain = div_result.rem;
    if (rate.mFilterLoops == 0.)
        rate.mFilterSlope = 0.;
    else
        rate.mFilterSlope = 1. / rate.mFilterLoops;
}


void sc_plugin_interface::initialize(server_arguments const& args, float* control_busses) {
    const bool nrt_mode = args.non_rt;

    done_nodes.reserve(64);
    pause_nodes.reserve(16);
    resume_nodes.reserve(16);
    freeAll_nodes.reserve(16);
    freeDeep_nodes.reserve(16);

    /* define functions */
    sc_interface.fDefineUnit = &define_unit;
    sc_interface.fDefineBufGen = &define_bufgen;
    sc_interface.fDefinePlugInCmd = &define_plugincmd;
    sc_interface.fDefineUnitCmd = &define_unitcmd;

    /* interface functions */
    sc_interface.fNodeEnd = &node_end;
    sc_interface.fGetNode = &get_node;
    sc_interface.fNodeRun = &node_set_run;
    sc_interface.fPrint = &print;
    sc_interface.fDoneAction = &done_action;
    sc_interface.fSendMsgFromRT = &send_message_from_RT;
    sc_interface.fSendMsgToRT = &send_message_to_RT;

    /* sndfile functions */
#ifdef NO_LIBSNDFILE
    sc_interface.fSndFileFormatInfoFromStrings = nullptr;
#else
    sc_interface.fSndFileFormatInfoFromStrings = &sndfileFormatInfoFromStrings;
#endif

    /* wave tables */
    sc_interface.mSine = gSine;
    sc_interface.mCosecant = gInvSine;
    sc_interface.mSineSize = kSineSize;
    sc_interface.mSineWavetable = gSineWavetable;

    /* memory allocation */
    if (!nrt_mode) {
        sc_interface.fRTAlloc = &rt_alloc;
        sc_interface.fRTRealloc = &rt_realloc;
        sc_interface.fRTFree = &rt_free;
    } else {
        sc_interface.fRTAlloc = &nrt_alloc_2;
        sc_interface.fRTRealloc = &nrt_realloc_2;
        sc_interface.fRTFree = &nrt_free_2;
    }

    sc_interface.fNRTAlloc = &nrt_alloc;
    sc_interface.fNRTRealloc = &nrt_realloc;
    sc_interface.fNRTFree = &nrt_free;

    /* ugen functions */
    sc_interface.fClearUnitOutputs = clear_outputs;

    /* buffer functions */
    sc_interface.fBufAlloc = &buf_alloc;

    /* trigger functions */
    sc_interface.fSendTrigger = &send_trigger;
    sc_interface.fSendNodeReply = &send_node_reply;

    /* world locks */
    sc_interface.fNRTLock = &world_lock;
    sc_interface.fNRTUnlock = &world_unlock;
    world.mNRTLock = new SC_Lock();

    /* fft library */
    sc_interface.fSCfftCreate = &scfft_create;
    sc_interface.fSCfftDestroy = &scfft_destroy;
    sc_interface.fSCfftDoFFT = &scfft_dofft;
    sc_interface.fSCfftDoIFFT = &scfft_doifft;

    /* scope API */
    sc_interface.fGetScopeBuffer = &get_scope_buffer;
    sc_interface.fPushScopeBuffer = &push_scope_buffer;
    sc_interface.fReleaseScopeBuffer = &release_scope_buffer;

    /* osc plugins */
    sc_interface.fDoAsynchronousCommand = &do_asynchronous_command;

    /* initialize world */
    /* control busses */
    world.mControlBus = control_busses;
    world.mNumControlBusChannels = args.control_busses;
    world.mControlBusTouched = new int32[args.control_busses];
    std::fill(world.mControlBusTouched, world.mControlBusTouched + args.control_busses, -1);

    /* audio busses */
    audio_busses.initialize(args.audio_busses, args.blocksize);

    world.mAudioBus = audio_busses.buffers;
    world.mNumAudioBusChannels = args.audio_busses;
    world.mAudioBusTouched = new int32[args.audio_busses];
    world.mAudioBusLocks = audio_busses.locks;
    world.mControlBusLock = new spin_lock();
    std::fill(world.mAudioBusTouched, world.mAudioBusTouched + args.audio_busses, -1);

    /* audio buffers */
    world.mNumSndBufs = args.buffers;
    world.mSndBufs = new SndBuf[world.mNumSndBufs];
    world.mSndBufsNonRealTimeMirror = new SndBuf[world.mNumSndBufs];
    world.mSndBufUpdates = new SndBufUpdates[world.mNumSndBufs];
    memset(world.mSndBufs, 0, world.mNumSndBufs * sizeof(SndBuf));
    memset(world.mSndBufsNonRealTimeMirror, 0, world.mNumSndBufs * sizeof(SndBuf));
    memset(world.mSndBufUpdates, 0, world.mNumSndBufs * sizeof(SndBufUpdates));
    world.mBufCounter = 0;

    async_buffer_guards.reset(new std::mutex[world.mNumSndBufs]);

    /* audio settings */
    world.mBufLength = args.blocksize;
    world.mSampleRate = args.samplerate;

    initialize_rate(world.mFullRate, args.samplerate, args.blocksize);
    initialize_rate(world.mBufRate, double(args.samplerate) / args.blocksize, 1);

    world.mNumInputs = args.input_channels;
    world.mNumOutputs = args.output_channels;

    world.mRealTime = !args.non_rt;
    world.mVerbosity = args.verbosity;

    /* rngs */
    world.mNumRGens = args.rng_count;
    world.mRGen = new RGen[world.mNumRGens];
    std::vector<std::uint32_t> seeds(world.mNumRGens);

    try {
        std::random_device rd;
        std::seed_seq seq({ rd(), rd(), rd() });
        seq.generate(seeds.begin(), seeds.end());
    } catch (...) {
        auto now = std::chrono::high_resolution_clock::now();

        auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch());

        std::seed_seq seq({ seconds.count() });
        seq.generate(seeds.begin(), seeds.end());
    }

    for (int i = 0; i < world.mNumRGens; ++i)
        world.mRGen[i].init(seeds[i]);
}

void sc_plugin_interface::reset_sampling_rate(int sr) {
    world.mSampleRate = sr;

    initialize_rate(world.mFullRate, sr, world.mBufLength);
    initialize_rate(world.mBufRate, double(sr) / world.mBufLength, 1);
}


void sc_done_action_handler::apply_done_actions(void) {
    for (server_node* node : done_nodes)
        instance->free_node(node);
    done_nodes.clear();

    for (server_node* node : resume_nodes)
        instance->node_resume(node);
    resume_nodes.clear();

    for (server_node* node : pause_nodes)
        instance->node_pause(node);
    pause_nodes.clear();

    for (abstract_group* group : freeDeep_nodes)
        instance->group_free_deep(group);
    freeDeep_nodes.clear();

    for (abstract_group* group : freeAll_nodes)
        instance->group_free_all(group);
    freeAll_nodes.clear();
}

sc_plugin_interface::~sc_plugin_interface(void) {
    delete[] world.mAudioBusTouched;
    delete[] world.mControlBusTouched;
    delete[] world.mSndBufs;
    delete[] world.mSndBufsNonRealTimeMirror;
    delete[] world.mSndBufUpdates;
    delete[] world.mRGen;
    delete reinterpret_cast<SC_Lock*>(world.mNRTLock);
}

namespace {

sample* allocate_buffer(size_t samples) {
    const size_t alloc_size = samples * sizeof(sample);
    sample* ret = (sample*)calloc_aligned(alloc_size);
    if (ret)
        mlock(ret, alloc_size);
    return ret;
}

inline int32 bufmask(int32 x) { return (1 << (31 - CLZ(x))) - 1; }

inline void sndbuf_init(SndBuf* buf) {
    buf->samplerate = 0;
    buf->sampledur = 0;
    buf->data = nullptr;
    buf->channels = 0;
    buf->samples = 0;
    buf->frames = 0;
    buf->mask = 0;
    buf->mask1 = 0;
    buf->coord = 0;
    buf->sndfile = nullptr;
    buf->isLocal = false;
}

inline void sndbuf_copy(SndBuf* dest, const SndBuf* src) {
    dest->samplerate = src->samplerate;
    dest->sampledur = src->sampledur;
    dest->data = src->data;
    dest->channels = src->channels;
    dest->samples = src->samples;
    dest->frames = src->frames;
    dest->mask = src->mask;
    dest->mask1 = src->mask1;
    dest->coord = src->coord;
    dest->sndfile = src->sndfile;
    dest->isLocal = src->isLocal;
}

static inline size_t compute_remaining_samples(size_t frames_per_read, size_t already_read, size_t total_frames) {
    int remain = frames_per_read;
    if (already_read + frames_per_read > total_frames)
        remain = total_frames - already_read;
    return remain;
}

#ifndef NO_LIBSNDFILE
void read_channel(SndfileHandle& sf, uint32_t channel_count, const uint32_t* channel_data, uint32 total_frames,
                  sample* data) {
    const unsigned int frames_per_read = 1024;
    sized_array<sample> read_frame(sf.channels() * frames_per_read);

    if (channel_count == 1) {
        // fast-path for single-channel read
        for (size_t i = 0; i < total_frames; i += frames_per_read) {
            int remaining_samples = compute_remaining_samples(frames_per_read, i, total_frames);
            size_t read = sf.readf(read_frame.c_array(), remaining_samples);

            size_t channel_mapping = channel_data[0];
            for (size_t frame = 0; frame != read; ++frame) {
                data[0] = read_frame[frame * sf.channels() + channel_mapping];
                data += channel_count;
            }
        }
    } else {
        for (size_t i = 0; i < total_frames; i += frames_per_read) {
            int remaining_samples = compute_remaining_samples(frames_per_read, i, total_frames);
            size_t read = sf.readf(read_frame.c_array(), remaining_samples);

            for (size_t frame = 0; frame != read; ++frame) {
                for (size_t c = 0; c != channel_count; ++c) {
                    size_t channel_mapping = channel_data[c];
                    data[c] = read_frame[frame * sf.channels() + channel_mapping];
                }
                data += channel_count;
            }
        }
    }
}
#endif

} /* namespace */

void sc_plugin_interface::allocate_buffer(SndBuf* buf, uint32_t frames, uint32_t channels, double samplerate) {
    const uint32_t samples = frames * channels;
    if (samples == 0)
        throw std::runtime_error("invalid buffer size");

    sample* data = nova::allocate_buffer(samples);
    if (data == nullptr)
        throw std::runtime_error("could not allocate memory");

    buf->data = data;
    buf->channels = channels;
    buf->frames = frames;
    buf->samples = samples;
    buf->mask = bufmask(samples); /* for delay lines */
    buf->mask1 = buf->mask - 1; /* for oscillators */
    buf->samplerate = samplerate;
    buf->sampledur = 1.0 / samplerate;
    buf->isLocal = false;
}

SndBuf* sc_plugin_interface::allocate_buffer(uint32_t index, uint32_t frames, uint32_t channels, double samplerate) {
    SndBuf* buf = World_GetNRTBuf(&world, index);
    allocate_buffer(buf, frames, channels, samplerate);
    return buf;
}

void sc_plugin_interface::buffer_read_alloc(uint32_t index, const char* filename, uint32_t start, uint32_t frames) {
#ifndef NO_LIBSNDFILE
    auto f = makeSndfileHandle(filename);
    if (f.rawHandle() == nullptr)
        throw std::runtime_error(f.strError());

    const size_t sf_frames = f.frames();

    if (start > sf_frames)
        start = sf_frames;

    if (frames == 0 || frames > sf_frames - start)
        frames = sf_frames - start;

    SndBuf* buf = World_GetNRTBuf(&world, index);
    allocate_buffer(buf, frames, f.channels(), f.samplerate());

    f.seek(start, SEEK_SET);
    f.readf(buf->data, frames);
#else
    throw std::runtime_error("cannot read buffer as supernova was compiled without libsndfile; buffer not allocated");
#endif
}


void sc_plugin_interface::buffer_alloc_read_channels(uint32_t index, const char* filename, uint32_t start,
                                                     uint32_t frames, uint32_t channel_count,
                                                     const uint32_t* channel_data) {
#ifndef NO_LIBSNDFILE
    // If no channel argument provided we read all channels.
    if (channel_count == 0) {
        buffer_read_alloc(index, filename, start, frames);
        return;
    }

    auto f = makeSndfileHandle(filename);
    if (f.rawHandle() == nullptr)
        throw std::runtime_error(f.strError());

    uint32_t sf_channels = uint32_t(f.channels());
    const uint32_t* max_chan = std::max_element(channel_data, channel_data + channel_count);
    if (*max_chan >= sf_channels)
        throw std::runtime_error("Channel out of range");

    const size_t sf_frames = f.frames();

    if (start > sf_frames)
        start = sf_frames;

    if (frames == 0 || frames > sf_frames - start)
        frames = sf_frames - start;

    SndBuf* buf = World_GetNRTBuf(&world, index);
    allocate_buffer(buf, frames, channel_count, f.samplerate());

    f.seek(start, SEEK_SET);
    read_channel(f, channel_count, channel_data, frames, buf->data);
#else
    throw std::runtime_error(
        "cannot read channel(s) as supernova was compiled without libsndfile; buffer not allocated");
#endif
}


void sc_plugin_interface::buffer_write(uint32_t index, const char* filename, const char* header_format,
                                       const char* sample_format, uint32_t start, uint32_t frames, bool leave_open) {
#ifndef NO_LIBSNDFILE
    SndBuf* buf = World_GetNRTBuf(&world, index);
    int format = headerFormatFromString(header_format) | sampleFormatFromString(sample_format);

    auto sf = makeSndfileHandle(filename, SFM_WRITE, format, buf->channels, buf->samplerate);

    if (sf.rawHandle() == nullptr)
        throw std::runtime_error(sf.strError());

    if (frames == 0xffffffff)
        frames = buf->frames;

    const uint32_t remain = uint32_t(buf->frames) > start ? buf->frames - start : 0;
    const uint32_t frames_to_write = std::min(remain, frames);

    if (frames_to_write)
        sf.writef(buf->data + start * buf->channels, frames_to_write);

    if (leave_open && !buf->sndfile)
        buf->sndfile = sf.takeOwnership();
#else
    throw std::runtime_error("cannot write to buffer as supernova was compiled without libsndfile");
#endif
}

#ifndef NO_LIBSNDFILE
static void buffer_read_verify(SndfileHandle& sf, size_t min_length, size_t samplerate, bool check_samplerate) {
    if (sf.rawHandle() == nullptr)
        throw std::runtime_error(sf.strError());
    if (sf.frames() < min_length)
        throw std::runtime_error("no more frames to read");

    if (check_samplerate && (sf.samplerate() != samplerate))
        throw std::runtime_error("sample rate mismatch");
}
#endif

void sc_plugin_interface::buffer_read(uint32_t index, const char* filename, uint32_t start_file, uint32_t frames,
                                      uint32_t start_buffer, bool leave_open) {
#ifndef NO_LIBSNDFILE
    SndBuf* buf = World_GetNRTBuf(&world, index);

    if (uint32_t(buf->frames) < start_buffer)
        throw std::runtime_error("buffer already full");

    auto sf = makeSndfileHandle(filename, SFM_READ);
    buffer_read_verify(sf, start_file, buf->samplerate, !leave_open);

    if (sf.channels() != buf->channels)
        throw std::runtime_error("channel count mismatch");

    const uint32_t buffer_remain = buf->frames - start_buffer;
    const uint32_t file_remain = sf.frames() - start_file;
    const uint32_t frames_to_read = std::min(frames, std::min(buffer_remain, file_remain));

    sf.seek(start_file, SEEK_SET);
    sf.readf(buf->data + start_buffer * buf->channels, frames_to_read);

    if (leave_open)
        buf->sndfile = sf.takeOwnership();
#else
    throw std::runtime_error("cannot read buffer as supernova was compiled without libsndfile");
#endif
}

void sc_plugin_interface::buffer_read_channel(uint32_t index, const char* filename, uint32_t start_file,
                                              uint32_t frames, uint32_t start_buffer, bool leave_open,
                                              uint32_t channel_count, const uint32_t* channel_data) {
#ifndef NO_LIBSNDFILE
    SndBuf* buf = World_GetNRTBuf(&world, index);

    if (channel_count != uint32_t(buf->channels))
        throw std::runtime_error("channel count mismatch");

    if (uint32_t(buf->frames) >= start_buffer)
        throw std::runtime_error("buffer already full");

    auto sf = makeSndfileHandle(filename, SFM_READ);
    buffer_read_verify(sf, start_file, buf->samplerate, !leave_open);

    uint32_t sf_channels = uint32_t(sf.channels());
    const uint32_t* max_chan = std::max_element(channel_data, channel_data + channel_count);
    if (*max_chan >= sf_channels)
        throw std::runtime_error("channel count mismatch");

    const uint32_t buffer_remain = buf->frames - start_buffer;
    const uint32_t file_remain = sf.frames() - start_file;

    const uint32_t frames_to_read = std::min(frames, std::min(buffer_remain, file_remain));

    sf.seek(start_file, SEEK_SET);
    read_channel(sf, channel_count, channel_data, frames_to_read, buf->data);

    if (leave_open)
        buf->sndfile = sf.takeOwnership();
#else
    throw std::runtime_error("cannot read buffer channel as supernova was compiled without libsndfile");
#endif
}

void sc_plugin_interface::buffer_close(uint32_t index) {
    SndBuf* buf = World_GetNRTBuf(&world, index);
#ifndef NO_LIBSNDFILE
    if (buf->sndfile == nullptr)
        return;
    sf_close(GETSNDFILE(buf));
#endif
    buf->sndfile = nullptr;
}


void sc_plugin_interface::buffer_zero(uint32_t index) {
    SndBuf* buf = World_GetNRTBuf(&world, index);

    uint32_t length = buf->frames * buf->channels;

    uint32_t unrolled = length & ~63;
    uint32_t remain = length & 63;

    zerovec_simd(buf->data, unrolled);
    zerovec(buf->data + unrolled, remain);
}

sample* sc_plugin_interface::buffer_generate(uint32_t buffer_index, const char* cmd_name, struct sc_msg_iter& msg) {
    return sc_factory->run_bufgen(&world, cmd_name, buffer_index, &msg);
}

void sc_plugin_interface::buffer_sync(uint32_t index) noexcept {
    sndbuf_copy(world.mSndBufs + index, world.mSndBufsNonRealTimeMirror + index);
    increment_write_updates(index);
}

void sc_plugin_interface::free_buffer(uint32_t index) {
    SndBuf* buf = world.mSndBufsNonRealTimeMirror + index;
#ifndef NO_LIBSNDFILE
    if (buf->sndfile)
        sf_close(GETSNDFILE(buf));
#endif
    sndbuf_init(buf);
}

} /* namespace nova */
