/*
 * Carla JSFX Plugin
 * Copyright (C) 2021 Filipe Coelho <falktx@falktx.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 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.
 *
 * For a full copy of the GNU General Public License see the doc/GPL.txt file.
 */

// TODO(jsfx) graphics section

#include "CarlaPluginInternal.hpp"
#include "CarlaEngine.hpp"
#include "CarlaJsfxUtils.hpp"
#include "CarlaBackendUtils.hpp"
#include "CarlaUtils.hpp"

#include "water/files/File.h"
#include "water/text/StringArray.h"

#include <algorithm>
#include <cstdio>
#include <cstring>

using water::CharPointer_UTF8;
using water::File;
using water::String;
using water::StringArray;

CARLA_BACKEND_START_NAMESPACE

// -------------------------------------------------------------------------------------------------------------------
// Fallback data

static const ExternalMidiNote kExternalMidiNoteFallback = { -1, 0, 0 };

// -------------------------------------------------------------------------------------------------------------------

class CarlaPluginJSFX : public CarlaPlugin
{
public:
    CarlaPluginJSFX(CarlaEngine* const engine, const uint id) noexcept
        : CarlaPlugin(engine, id),
          fEffect(nullptr),
          fEffectState(nullptr),
          fUnit(),
          fChunkText(),
          fTransportValues(),
          fMapOfSliderToParameter(ysfx_max_sliders, -1)
    {
        carla_debug("CarlaPluginJSFX::CarlaPluginJSFX(%p, %i)", engine, id);

        carla_zeroStruct(fTransportValues);
    }

    ~CarlaPluginJSFX() noexcept override
    {
        carla_debug("CarlaPluginJSFX::~CarlaPluginJSFX()");

        pData->singleMutex.lock();
        pData->masterMutex.lock();

        if (pData->client != nullptr && pData->client->isActive())
            pData->client->deactivate(true);

        if (pData->active)
        {
            deactivate();
            pData->active = false;
        }

        clearBuffers();

        ysfx_state_free(fEffectState);
        ysfx_free(fEffect);
    }

    // -------------------------------------------------------------------
    // Information (base)

    PluginType getType() const noexcept override
    {
        return PLUGIN_JSFX;
    }

    PluginCategory getCategory() const noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr, CarlaPlugin::getCategory());

        return CarlaJsfxCategories::getFromEffect(fEffect);
    }

    uint32_t getLatencyInFrames() const noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr, 0);

        ysfx_real sampleRate = ysfx_get_sample_rate(fEffect);
        ysfx_real latencyInSeconds = ysfx_get_pdc_delay(fEffect);

        //NOTE: `pdc_bot_ch` and `pdc_top_ch` channel range ignored

        int32_t latencyInFrames = water::roundToInt(latencyInSeconds * sampleRate);
        wassert(latencyInFrames >= 0);

        return (uint32_t)latencyInFrames;
    }

    // -------------------------------------------------------------------
    // Information (count)

    uint32_t getMidiInCount() const noexcept override
    {
        return 1;
    }

    uint32_t getMidiOutCount() const noexcept override
    {
        return 1;
    }

    uint32_t getParameterScalePointCount(const uint32_t parameterId) const noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count, 0);

        const uint32_t rindex = static_cast<uint32_t>(pData->param.data[parameterId].rindex);
        return ysfx_slider_get_enum_names(fEffect, rindex, nullptr, 0);;
    }

    // -------------------------------------------------------------------
    // Information (current data)

    std::size_t getChunkData(void** const dataPtr) noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(pData->options & PLUGIN_OPTION_USE_CHUNKS, 0);
        CARLA_SAFE_ASSERT_RETURN(dataPtr != nullptr, 0);

        ysfx_state_free(fEffectState);
        fEffectState = ysfx_save_state(fEffect);
        CARLA_SAFE_ASSERT_RETURN(fEffectState != nullptr, 0);

        *dataPtr = fEffectState->data;
        return fEffectState->data_size;
    }

    // -------------------------------------------------------------------
    // Information (per-plugin data)

    uint getOptionsAvailable() const noexcept override
    {
        uint options = 0x0;

        options |= PLUGIN_OPTION_USE_CHUNKS;

        options |= PLUGIN_OPTION_SEND_CONTROL_CHANGES;
        options |= PLUGIN_OPTION_SEND_CHANNEL_PRESSURE;
        options |= PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH;
        options |= PLUGIN_OPTION_SEND_PITCHBEND;
        options |= PLUGIN_OPTION_SEND_ALL_SOUND_OFF;
        options |= PLUGIN_OPTION_SEND_PROGRAM_CHANGES;
        options |= PLUGIN_OPTION_SKIP_SENDING_NOTES;

        return options;
    }

    float getParameterValue(const uint32_t parameterId) const noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count, 0.0f);

        const uint32_t rindex = static_cast<uint32_t>(pData->param.data[parameterId].rindex);
        return static_cast<float>(ysfx_slider_get_value(fEffect, rindex));
    }

    bool getParameterName(const uint32_t parameterId, char* const strBuf) const noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr, false);
        CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count, false);

        const uint32_t rindex = static_cast<uint32_t>(pData->param.data[parameterId].rindex);

        if (const char* const name = ysfx_slider_get_name(fEffect, rindex))
        {
            std::snprintf(strBuf, STR_MAX, "%s", name);
            return true;
        }

        return false;
    }

    float getParameterScalePointValue(const uint32_t parameterId, const uint32_t scalePointId) const noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(parameterId < getParameterCount(), 0.0f);
        CARLA_SAFE_ASSERT_RETURN(scalePointId < getParameterScalePointCount(parameterId), 0.0f);
        return (float)scalePointId;
    }

    bool getParameterScalePointLabel(const uint32_t parameterId, const uint32_t scalePointId, char* const strBuf) const noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(parameterId < getParameterCount(), false);

        const uint32_t rindex = static_cast<uint32_t>(pData->param.data[parameterId].rindex);

        const uint32_t enumCount = ysfx_slider_get_enum_names(fEffect, rindex, nullptr, 0);
        CARLA_SAFE_ASSERT_RETURN(scalePointId < enumCount, false);

        if (const char* const name = ysfx_slider_get_enum_name(fEffect, rindex, scalePointId))
        {
            std::snprintf(strBuf, STR_MAX, "%s", name);
            return true;
        }

        return false;
    }

    bool getLabel(char* const strBuf) const noexcept override
    {
        std::strncpy(strBuf, fUnit.getFileId().toRawUTF8(), STR_MAX);
        return true;
    }

    // -------------------------------------------------------------------
    // Set data (plugin-specific stuff)

    void setParameterValue(const uint32_t parameterId, const float value, const bool sendGui, const bool sendOsc, const bool sendCallback) noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr,);
        CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count,);

        const uint32_t rindex = static_cast<uint32_t>(pData->param.data[parameterId].rindex);
        ysfx_slider_set_value(fEffect, rindex, value);

        CarlaPlugin::setParameterValue(parameterId, value, sendGui, sendOsc, sendCallback);
    }

    void setParameterValueRT(const uint32_t parameterId, const float value, const uint32_t frameOffset, const bool sendCallbackLater) noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr,);
        CARLA_SAFE_ASSERT_RETURN(parameterId < pData->param.count,);

        const uint32_t rindex = static_cast<uint32_t>(pData->param.data[parameterId].rindex);
        ysfx_slider_set_value(fEffect, rindex, value);

        CarlaPlugin::setParameterValueRT(parameterId, value, frameOffset, sendCallbackLater);
    }

    void setChunkData(const void* data, std::size_t dataSize) override
    {
        CARLA_SAFE_ASSERT_RETURN(pData->options & PLUGIN_OPTION_USE_CHUNKS,);

        ysfx_state_t state;
        state.sliders = nullptr;
        state.slider_count = 0;
        state.data = static_cast<uint8_t*>(const_cast<void*>(data));
        state.data_size = dataSize;

        CARLA_SAFE_ASSERT(ysfx_load_state(fEffect, &state));
    }

    // -------------------------------------------------------------------
    // Plugin state

    void reload() override
    {
        CARLA_SAFE_ASSERT_RETURN(pData->engine != nullptr,);
        CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr,);
        carla_debug("CarlaPluginJSFX::reload()");

        const EngineProcessMode processMode(pData->engine->getProccessMode());

        // Safely disable plugin for reload
        const ScopedDisabler sd(this);

        if (pData->active)
            deactivate();

        clearBuffers();

        // ---------------------------------------------------------------

        // initialize the block size and sample rate
        // loading the chunk can invoke @slider which makes computations based on these
        ysfx_set_sample_rate(fEffect, pData->engine->getSampleRate());
        ysfx_set_block_size(fEffect, (uint32_t)pData->engine->getBufferSize());
        ysfx_init(fEffect);

        const uint32_t aIns = ysfx_get_num_inputs(fEffect);
        const uint32_t aOuts = ysfx_get_num_outputs(fEffect);

        // perhaps we obtained a latency value from @init
        pData->client->setLatency(getLatencyInFrames());

        if (aIns > 0)
        {
            pData->audioIn.createNew(aIns);
        }

        if (aOuts > 0)
        {
            pData->audioOut.createNew(aOuts);
        }

        // count the sliders and establish the mappings between parameter and slider
        uint32_t params = 0;
        uint32_t mapOfParameterToSlider[ysfx_max_sliders];
        for (uint32_t rindex = 0; rindex < ysfx_max_sliders; ++rindex)
        {
            if (ysfx_slider_exists(fEffect, rindex))
            {
                mapOfParameterToSlider[params] = rindex;
                fMapOfSliderToParameter[rindex] = (int32_t)params;
                ++params;
            }
            else
            {
                fMapOfSliderToParameter[rindex] = -1;
            }
        }

        if (params > 0)
        {
            pData->param.createNew(params, false);
        }

        const uint portNameSize = pData->engine->getMaxPortNameSize();
        CarlaString portName;

        // Audio Ins
        for (uint32_t j = 0; j < aIns; ++j)
        {
            portName.clear();

            if (processMode == ENGINE_PROCESS_MODE_SINGLE_CLIENT)
            {
                portName  = pData->name;
                portName += ":";
            }

            const char* const inputName = ysfx_get_input_name(fEffect, j);
            if (inputName && inputName[0])
            {
                portName += inputName;
            }
            else if (aIns > 1)
            {
                portName += "input_";
                portName += CarlaString(j+1);
            }
            else
                portName += "input";

            portName.truncate(portNameSize);

            pData->audioIn.ports[j].port   = (CarlaEngineAudioPort*)pData->client->addPort(kEnginePortTypeAudio, portName, true, j);
            pData->audioIn.ports[j].rindex = j;
        }

        // Audio Outs
        for (uint32_t j = 0; j < aOuts; ++j)
        {
            portName.clear();

            if (processMode == ENGINE_PROCESS_MODE_SINGLE_CLIENT)
            {
                portName  = pData->name;
                portName += ":";
            }

            const char* const outputName = ysfx_get_input_name(fEffect, j);
            if (outputName && outputName[0])
            {
                portName += outputName;
            }
            else if (aOuts > 1)
            {
                portName += "output_";
                portName += CarlaString(j+1);
            }
            else
                portName += "output";

            portName.truncate(portNameSize);

            pData->audioOut.ports[j].port   = (CarlaEngineAudioPort*)pData->client->addPort(kEnginePortTypeAudio, portName, false, j);
            pData->audioOut.ports[j].rindex = j;
        }

        // Parameters
        for (uint32_t j = 0; j < params; ++j)
        {
            const uint32_t rindex = mapOfParameterToSlider[j];
            pData->param.data[j].type   = PARAMETER_INPUT;
            pData->param.data[j].index  = (int32_t)j;
            pData->param.data[j].rindex = (int32_t)rindex;

            ysfx_slider_range_t range = {};
            ysfx_slider_get_range(fEffect, rindex, &range);

            float min = (float)range.min;
            float max = (float)range.max;
            float def = (float)range.def;
            float step = (float)range.inc;
            float stepSmall;
            float stepLarge;

            // only use values as integer if we have a proper range
            const bool isEnum = ysfx_slider_is_enum(fEffect, rindex) &&
                                carla_isZero(min) &&
                                max >= 0.0f &&
                                carla_isEqual(max + 1.0f, static_cast<float>(ysfx_slider_get_enum_names(fEffect, rindex, nullptr, 0)));

            // NOTE: in case of incomplete slider specification without <min,max,step>;
            //  these are usually output-only sliders.
            if (carla_isEqual(min, max))
            {
                // replace with a dummy range
                min = 0.0f;
                max = 1.0f;
            }

            if (min > max)
                std::swap(min, max);

            if (def < min)
                def = min;
            else if (def > max)
                def = max;

            pData->param.data[j].hints |= PARAMETER_IS_ENABLED;

            if (isEnum)
            {
                step = 1.0f;
                stepSmall = 1.0f;
                stepLarge = 10.0f;
                pData->param.data[j].hints |= PARAMETER_IS_INTEGER;
                pData->param.data[j].hints |= PARAMETER_USES_SCALEPOINTS;
            }
            else
            {
                stepSmall = step/10.0f;
                stepLarge = step*10.0f;
                pData->param.data[j].hints |= PARAMETER_CAN_BE_CV_CONTROLLED;
            }

            pData->param.ranges[j].min = min;
            pData->param.ranges[j].max = max;
            pData->param.ranges[j].def = def;
            pData->param.ranges[j].step = step;
            pData->param.ranges[j].stepSmall = stepSmall;
            pData->param.ranges[j].stepLarge = stepLarge;
        }

        //if (needsCtrlIn)
        {
            portName.clear();

            if (processMode == ENGINE_PROCESS_MODE_SINGLE_CLIENT)
            {
                portName  = pData->name;
                portName += ":";
            }

            portName += "events-in";
            portName.truncate(portNameSize);

            pData->event.portIn = (CarlaEngineEventPort*)pData->client->addPort(kEnginePortTypeEvent, portName, true, 0);
        }

        //if (needsCtrlOut)
        {
            portName.clear();

            if (processMode == ENGINE_PROCESS_MODE_SINGLE_CLIENT)
            {
                portName  = pData->name;
                portName += ":";
            }

            portName += "events-out";
            portName.truncate(portNameSize);

            pData->event.portOut = (CarlaEngineEventPort*)pData->client->addPort(kEnginePortTypeEvent, portName, false, 0);
        }
    }

    // -------------------------------------------------------------------
    // Plugin processing

    void activate() noexcept override
    {
        CARLA_SAFE_ASSERT_RETURN(fEffect,);

        ysfx_set_sample_rate(fEffect, pData->engine->getSampleRate());
        ysfx_set_block_size(fEffect, (uint32_t)pData->engine->getBufferSize());
        ysfx_init(fEffect);

        fTransportValues.tempo = 120;
        fTransportValues.playback_state = ysfx_playback_paused;
        fTransportValues.time_position = 0;
        fTransportValues.beat_position = 0;
        fTransportValues.time_signature[0] = 4;
        fTransportValues.time_signature[1] = 4;
    }

    void process(const float* const* const audioIn, float** const audioOut,
                 const float* const* const, float**,
                 const uint32_t frames) override
    {
        CARLA_SAFE_ASSERT_RETURN(fEffect,);

        // --------------------------------------------------------------------------------------------------------
        // Set TimeInfo

        const EngineTimeInfo timeInfo = pData->engine->getTimeInfo();
        const EngineTimeInfoBBT& bbt = timeInfo.bbt;

        fTransportValues.playback_state = timeInfo.playing ?
            ysfx_playback_playing : ysfx_playback_paused;
        fTransportValues.time_position = 1e-6*double(timeInfo.usecs);

        if (bbt.valid)
        {
            const double samplePos  = double(timeInfo.frame);
            const double sampleRate = pData->engine->getSampleRate();
            fTransportValues.tempo = bbt.beatsPerMinute;
            fTransportValues.beat_position = samplePos / (sampleRate * 60 / bbt.beatsPerMinute);
            fTransportValues.time_signature[0] = (uint32_t)bbt.beatsPerBar;
            fTransportValues.time_signature[1] = (uint32_t)bbt.beatType;
        }

        ysfx_set_time_info(fEffect, &fTransportValues);

        // --------------------------------------------------------------------------------------------------------
        // Event Input and Processing

        if (pData->event.portIn != nullptr)
        {
            // ----------------------------------------------------------------------------------------------------
            // MIDI Input (External)

            if (pData->extNotes.mutex.tryLock())
            {
                for (RtLinkedList<ExternalMidiNote>::Itenerator it = pData->extNotes.data.begin2(); it.valid(); it.next())
                {
                    const ExternalMidiNote& note(it.getValue(kExternalMidiNoteFallback));
                    CARLA_SAFE_ASSERT_CONTINUE(note.channel >= 0 && note.channel < MAX_MIDI_CHANNELS);

                    uint8_t midiData[3];
                    midiData[0] = uint8_t((note.velo > 0 ? MIDI_STATUS_NOTE_ON : MIDI_STATUS_NOTE_OFF) | (note.channel & MIDI_CHANNEL_BIT));
                    midiData[1] = note.note;
                    midiData[2] = note.velo;

                    ysfx_midi_event_t event;
                    event.bus = 0;
                    event.offset = 0;
                    event.size = 3;
                    event.data = midiData;
                    ysfx_send_midi(fEffect, &event);
                }

                pData->extNotes.data.clear();
                pData->extNotes.mutex.unlock();

            } // End of MIDI Input (External)

            // ----------------------------------------------------------------------------------------------------
            // Event Input (System)

#ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH
            bool allNotesOffSent = false;
#endif

            for (uint32_t i=0, numEvents=pData->event.portIn->getEventCount(); i < numEvents; ++i)
            {
                EngineEvent& event(pData->event.portIn->getEvent(i));

                if (event.time >= frames)
                    continue;

                switch (event.type)
                {
                case kEngineEventTypeNull:
                    break;

                case kEngineEventTypeControl: {
                    EngineControlEvent& ctrlEvent(event.ctrl);

                    switch (ctrlEvent.type)
                    {
                    case kEngineControlEventTypeNull:
                        break;

                    case kEngineControlEventTypeParameter: {
                        float value;

#ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH
                        // non-midi
                        if (event.channel == kEngineEventNonMidiChannel)
                        {
                            const uint32_t k = ctrlEvent.param;
                            CARLA_SAFE_ASSERT_CONTINUE(k < pData->param.count);

                            ctrlEvent.handled = true;
                            value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.normalizedValue);
                            setParameterValueRT(k, value, event.time, true);
                            continue;
                        }

                        // Control backend stuff
                        if (event.channel == pData->ctrlChannel)
                        {
                            if (MIDI_IS_CONTROL_BREATH_CONTROLLER(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_DRYWET) != 0)
                            {
                                ctrlEvent.handled = true;
                                value = ctrlEvent.normalizedValue;
                                setDryWetRT(value, true);
                            }
                            else if (MIDI_IS_CONTROL_CHANNEL_VOLUME(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_VOLUME) != 0)
                            {
                                ctrlEvent.handled = true;
                                value = ctrlEvent.normalizedValue*127.0f/100.0f;
                                setVolumeRT(value, true);
                            }
                            else if (MIDI_IS_CONTROL_BALANCE(ctrlEvent.param) && (pData->hints & PLUGIN_CAN_BALANCE) != 0)
                            {
                                float left, right;
                                value = ctrlEvent.normalizedValue/0.5f - 1.0f;

                                if (value < 0.0f)
                                {
                                    left  = -1.0f;
                                    right = (value*2.0f)+1.0f;
                                }
                                else if (value > 0.0f)
                                {
                                    left  = (value*2.0f)-1.0f;
                                    right = 1.0f;
                                }
                                else
                                {
                                    left  = -1.0f;
                                    right = 1.0f;
                                }

                                ctrlEvent.handled = true;
                                setBalanceLeftRT(left, true);
                                setBalanceRightRT(right, true);
                            }
                        }
#endif
                        // Control plugin parameters
                        uint32_t k;
                        for (k=0; k < pData->param.count; ++k)
                        {
                            if (pData->param.data[k].midiChannel != event.channel)
                                continue;
                            if (pData->param.data[k].mappedControlIndex != ctrlEvent.param)
                                continue;
                            if (pData->param.data[k].type != PARAMETER_INPUT)
                                continue;
                            if ((pData->param.data[k].hints & PARAMETER_IS_AUTOMABLE) == 0)
                                continue;

                            ctrlEvent.handled = true;
                            value = pData->param.getFinalUnnormalizedValue(k, ctrlEvent.normalizedValue);
                            setParameterValueRT(k, value, event.time, true);
                        }

                        if ((pData->options & PLUGIN_OPTION_SEND_CONTROL_CHANGES) != 0 && ctrlEvent.param < MAX_MIDI_VALUE)
                        {
                            uint8_t midiData[3];
                            midiData[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
                            midiData[1] = uint8_t(ctrlEvent.param);
                            midiData[2] = uint8_t(ctrlEvent.normalizedValue*127.0f);

                            ysfx_midi_event_t yevent;
                            yevent.bus = 0;
                            yevent.offset = event.time;
                            yevent.size = 3;
                            yevent.data = midiData;
                            ysfx_send_midi(fEffect, &yevent);
                        }

#ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH
                        if (! ctrlEvent.handled)
                            checkForMidiLearn(event);
#endif
                        break;
                    } // case kEngineControlEventTypeParameter

                    case kEngineControlEventTypeMidiBank:
                        if ((pData->options & PLUGIN_OPTION_SEND_PROGRAM_CHANGES) != 0)
                        {
                            uint8_t midiData[3];
                            midiData[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
                            midiData[1] = MIDI_CONTROL_BANK_SELECT;
                            midiData[2] = 0;

                            ysfx_midi_event_t yevent;
                            yevent.bus = 0;
                            yevent.offset = event.time;
                            yevent.size = 3;
                            yevent.data = midiData;
                            ysfx_send_midi(fEffect, &yevent);

                            midiData[1] = MIDI_CONTROL_BANK_SELECT__LSB;
                            midiData[2] = uint8_t(ctrlEvent.normalizedValue*127.0f);
                            yevent.bus = 0;
                            yevent.offset = event.time;
                            yevent.size = 3;
                            yevent.data = midiData;
                            ysfx_send_midi(fEffect, &yevent);
                        }
                        break;

                    case kEngineControlEventTypeMidiProgram:
                        if (event.channel == pData->ctrlChannel && (pData->options & PLUGIN_OPTION_MAP_PROGRAM_CHANGES) != 0)
                        {
                            if (ctrlEvent.param < pData->prog.count)
                            {
                                setProgramRT(ctrlEvent.param, true);
                            }
                        }
                        else if ((pData->options & PLUGIN_OPTION_SEND_PROGRAM_CHANGES) != 0)
                        {
                            uint8_t midiData[2];
                            midiData[0] = uint8_t(MIDI_STATUS_PROGRAM_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
                            midiData[1] = uint8_t(ctrlEvent.normalizedValue*127.0f);
                            ysfx_midi_event_t yevent;
                            yevent.bus = 0;
                            yevent.offset = event.time;
                            yevent.size = 2;
                            yevent.data = midiData;
                            ysfx_send_midi(fEffect, &yevent);
                        }
                        break;

                    case kEngineControlEventTypeAllSoundOff:
                        if (pData->options & PLUGIN_OPTION_SEND_ALL_SOUND_OFF)
                        {
                            uint8_t midiData[3];
                            midiData[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
                            midiData[1] = MIDI_CONTROL_ALL_SOUND_OFF;
                            midiData[2] = 0;
                            ysfx_midi_event_t yevent;
                            yevent.bus = 0;
                            yevent.offset = event.time;
                            yevent.size = 3;
                            yevent.data = midiData;
                            ysfx_send_midi(fEffect, &yevent);
                        }
                        break;

                    case kEngineControlEventTypeAllNotesOff:
                        if (pData->options & PLUGIN_OPTION_SEND_ALL_SOUND_OFF)
                        {
#ifndef BUILD_BRIDGE_ALTERNATIVE_ARCH
                            if (event.channel == pData->ctrlChannel && ! allNotesOffSent)
                            {
                                allNotesOffSent = true;
                                postponeRtAllNotesOff();
                            }
#endif

                            uint8_t midiData[3];
                            midiData[0] = uint8_t(MIDI_STATUS_CONTROL_CHANGE | (event.channel & MIDI_CHANNEL_BIT));
                            midiData[1] = MIDI_CONTROL_ALL_NOTES_OFF;
                            midiData[2] = 0;

                            ysfx_midi_event_t yevent;
                            yevent.bus = 0;
                            yevent.offset = event.time;
                            yevent.size = 3;
                            yevent.data = midiData;
                            ysfx_send_midi(fEffect, &yevent);
                        }
                        break;
                    } // switch (ctrlEvent.type)
                    break;
                } // case kEngineEventTypeControl

                case kEngineEventTypeMidi: {
                    const EngineMidiEvent& midiEvent(event.midi);

                    const uint8_t* const midiData(midiEvent.size > EngineMidiEvent::kDataSize ? midiEvent.dataExt : midiEvent.data);

                    uint8_t status = uint8_t(MIDI_GET_STATUS_FROM_DATA(midiData));

                    if ((status == MIDI_STATUS_NOTE_OFF || status == MIDI_STATUS_NOTE_ON) && (pData->options & PLUGIN_OPTION_SKIP_SENDING_NOTES))
                        continue;
                    if (status == MIDI_STATUS_CHANNEL_PRESSURE && (pData->options & PLUGIN_OPTION_SEND_CHANNEL_PRESSURE) == 0)
                        continue;
                    if (status == MIDI_STATUS_CONTROL_CHANGE && (pData->options & PLUGIN_OPTION_SEND_CONTROL_CHANGES) == 0)
                        continue;
                    if (status == MIDI_STATUS_POLYPHONIC_AFTERTOUCH && (pData->options & PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH) == 0)
                        continue;
                    if (status == MIDI_STATUS_PITCH_WHEEL_CONTROL && (pData->options & PLUGIN_OPTION_SEND_PITCHBEND) == 0)
                        continue;

                    // Fix bad note-off
                    if (status == MIDI_STATUS_NOTE_ON && midiData[2] == 0)
                        status = MIDI_STATUS_NOTE_OFF;

                    // put back channel in data
                    uint8_t midiData2[midiEvent.size];
                    midiData2[0] = uint8_t(status | (event.channel & MIDI_CHANNEL_BIT));
                    std::memcpy(midiData2+1, midiData+1, static_cast<std::size_t>(midiEvent.size-1));

                    ysfx_midi_event_t yevent;
                    yevent.bus = midiEvent.port;
                    yevent.offset = event.time;
                    yevent.size = midiEvent.size;
                    yevent.data = midiData;
                    ysfx_send_midi(fEffect, &yevent);

                    if (status == MIDI_STATUS_NOTE_ON)
                    {
                        pData->postponeNoteOnRtEvent(true, event.channel, midiData[1], midiData[2]);
                    }
                    else if (status == MIDI_STATUS_NOTE_OFF)
                    {
                        pData->postponeNoteOffRtEvent(true, event.channel, midiData[1]);
                    }
                } break;
                } // switch (event.type)
            }

            pData->postRtEvents.trySplice();

        } // End of Event Input and Processing

        // --------------------------------------------------------------------------------------------------------
        // Plugin processing

        const uint32_t numInputs = ysfx_get_num_inputs(fEffect);
        const uint32_t numOutputs = ysfx_get_num_outputs(fEffect);
        ysfx_process_float(fEffect, audioIn, audioOut, numInputs, numOutputs, frames);

        // End of Plugin processing (no events)

        // --------------------------------------------------------------------------------------------------------
        // MIDI Output

        if (pData->event.portOut != nullptr)
        {
            ysfx_midi_event_t event;

            while (ysfx_receive_midi(fEffect, &event))
            {
                CARLA_SAFE_ASSERT_BREAK(event.offset < frames);
                CARLA_SAFE_ASSERT_BREAK(event.size > 0);
                CARLA_SAFE_ASSERT_CONTINUE(event.size <= 0xff);

                if (! pData->event.portOut->writeMidiEvent(event.offset,
                                                           static_cast<uint8_t>(event.size),
                                                           event.data))
                    break;
            }

        } // End of MIDI Output

        // --------------------------------------------------------------------------------------------------------
        // Control Output

        {
            uint64_t changes = ysfx_fetch_slider_changes(fEffect);
            uint64_t automations = ysfx_fetch_slider_automations(fEffect);

            if ((changes|automations) != 0)
            {
                for (uint32_t rindex = 0; rindex < ysfx_max_sliders; ++rindex)
                {
                    uint64_t mask = (uint64_t)1 << rindex;

                    //TODO: automations and changes are handled identically
                    // refer to `sliderchange` vs `slider_automate`

                    if (((changes|automations) & mask) != 0)
                    {
                        int32_t parameterIndex = fMapOfSliderToParameter[rindex];
                        CARLA_SAFE_ASSERT_CONTINUE(parameterIndex != -1);

                        const float newValue = static_cast<float>(ysfx_slider_get_value(fEffect, (uint32_t)parameterIndex));
                        setParameterValueRT((uint32_t)parameterIndex, newValue, 0, true);
                    }
                }
            }

            //TODO: slider visibility changes, if this feature can be supported
        }
    }

    // -------------------------------------------------------------------

    bool initJSFX(const CarlaPluginPtr plugin,
                  const char* const filename, const char* name, const char* const label, const uint options)
    {
        CARLA_SAFE_ASSERT_RETURN(pData->engine != nullptr, false);

        // ---------------------------------------------------------------
        // first checks

        if (pData->client != nullptr)
        {
            pData->engine->setLastError("Plugin client is already registered");
            return false;
        }

        if ((filename == nullptr || filename[0] == '\0') &&
            (label == nullptr || label[0] == '\0'))
        {
            pData->engine->setLastError("null filename and label");
            return false;
        }

        // ---------------------------------------------------------------

        fUnit = CarlaJsfxUnit();

        {
            StringArray splitPaths;

            if (const char* paths = pData->engine->getOptions().pathJSFX)
                splitPaths = StringArray::fromTokens(CharPointer_UTF8(paths), CARLA_OS_SPLIT_STR, "");

            File file;
            if (filename && filename[0] != '\0')
                file = File(CharPointer_UTF8(filename));

            if (file.isNotNull() && file.existsAsFile())
            {
                // find which engine search path we're in, and use this as the root
                for (int i = 0; i < splitPaths.size() && !fUnit; ++i)
                {
                    const File currentPath(splitPaths[i]);
                    if (file.isAChildOf(currentPath))
                        fUnit = CarlaJsfxUnit(currentPath, file);
                }

                // if not found in engine search paths, use parent directory as the root
                if (! fUnit)
                    fUnit = CarlaJsfxUnit(file.getParentDirectory(), file);
            }
            else if (label && label[0] != '\0')
            {
                // search a matching file in plugin paths
                for (int i = 0; i < splitPaths.size() && !fUnit; ++i)
                {
                    const File currentPath(splitPaths[i]);
                    const File currentFile = currentPath.getChildFile(CharPointer_UTF8(label));
                    const CarlaJsfxUnit currentUnit(currentPath, currentFile);
                    if (File(currentUnit.getFilePath()).existsAsFile())
                        fUnit = currentUnit;
                }
            }
        }

        if (! fUnit)
        {
            pData->engine->setLastError("Cannot locate the JSFX plugin");
            return false;
        }

        // ---------------------------------------------------------------

        ysfx_config_u config(ysfx_config_new());
        CARLA_SAFE_ASSERT_RETURN(config != nullptr, false);

        const water::String rootPath = fUnit.getRootPath();
        const water::String filePath = fUnit.getFilePath();

        ysfx_register_builtin_audio_formats(config.get());
        ysfx_set_import_root(config.get(), rootPath.toRawUTF8());
        ysfx_guess_file_roots(config.get(), filePath.toRawUTF8());
        ysfx_set_log_reporter(config.get(), &CarlaJsfxLogging::logAll);
        ysfx_set_user_data(config.get(), (intptr_t)this);

        fEffect = ysfx_new(config.get());
        CARLA_SAFE_ASSERT_RETURN(fEffect != nullptr, false);

        // ---------------------------------------------------------------
        // get info

        {
            if (! ysfx_load_file(fEffect, filePath.toRawUTF8(), 0))
            {
                pData->engine->setLastError("Failed to load JSFX");
                return false;
            }

            // TODO(jsfx) adapt when implementing these features
            const int compileFlags = 0
                //| ysfx_compile_no_serialize
                | ysfx_compile_no_gfx
                ;

            if (! ysfx_compile(fEffect, compileFlags))
            {
                pData->engine->setLastError("Failed to compile JSFX");
                return false;
            }
        }

        if (name != nullptr && name[0] != '\0')
        {
            pData->name = pData->engine->getUniquePluginName(name);
        }
        else
        {
            pData->name = carla_strdup(ysfx_get_name(fEffect));
        }

        pData->filename = carla_strdup(filePath.toRawUTF8());

        // ---------------------------------------------------------------
        // register client

        pData->client = pData->engine->addClient(plugin);

        if (pData->client == nullptr || ! pData->client->isOk())
        {
            pData->engine->setLastError("Failed to register plugin client");
            return false;
        }

        // ---------------------------------------------------------------
        // set options

        pData->options = 0x0;

        if (isPluginOptionEnabled(options, PLUGIN_OPTION_USE_CHUNKS))
            pData->options |= PLUGIN_OPTION_USE_CHUNKS;

        if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_CONTROL_CHANGES))
            pData->options |= PLUGIN_OPTION_SEND_CONTROL_CHANGES;
        if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_CHANNEL_PRESSURE))
            pData->options |= PLUGIN_OPTION_SEND_CHANNEL_PRESSURE;
        if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_PITCHBEND))
            pData->options |= PLUGIN_OPTION_SEND_PITCHBEND;
        if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_ALL_SOUND_OFF))
            pData->options |= PLUGIN_OPTION_SEND_ALL_SOUND_OFF;
        if (isPluginOptionEnabled(options, PLUGIN_OPTION_MAP_PROGRAM_CHANGES))
            pData->options |= PLUGIN_OPTION_MAP_PROGRAM_CHANGES;
        if (isPluginOptionInverseEnabled(options, PLUGIN_OPTION_SKIP_SENDING_NOTES))
            pData->options |= PLUGIN_OPTION_SKIP_SENDING_NOTES;
        if (isPluginOptionEnabled(options, PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH))
            pData->options |= PLUGIN_OPTION_SEND_NOTE_AFTERTOUCH;

        return true;
    }

private:
    ysfx_t* fEffect;
    ysfx_state_t* fEffectState;
    CarlaJsfxUnit fUnit;
    water::String fChunkText;
    ysfx_time_info_t fTransportValues;
    std::vector<int32_t> fMapOfSliderToParameter;

    CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CarlaPluginJSFX)
};

// -------------------------------------------------------------------------------------------------------------------

CarlaPluginPtr CarlaPlugin::newJSFX(const Initializer& init)
{
    carla_debug("CarlaPlugin::newJSFX({%p, \"%s\", \"%s\", \"%s\", " P_INT64 "})",
                init.engine, init.filename, init.name, init.label, init.uniqueId);

    std::shared_ptr<CarlaPluginJSFX> plugin(new CarlaPluginJSFX(init.engine, init.id));

    if (! plugin->initJSFX(plugin, init.filename, init.name, init.label, init.options))
        return nullptr;

    return plugin;
}

// -------------------------------------------------------------------------------------------------------------------

CARLA_BACKEND_END_NAMESPACE
