/*
 * DISTRHO Plugin Framework (DPF)
 * Copyright (C) 2012-2022 Filipe Coelho <falktx@falktx.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * 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 Lesser General Public License for more details.
 *
 * For a full copy of the license see the LGPL.txt file
 */

#include "DistrhoPluginInternal.hpp"

#if !defined(DISTRHO_OS_WINDOWS) && !defined(STATIC_BUILD)
# include "../DistrhoPluginUtils.hpp"
#endif

#if DISTRHO_PLUGIN_HAS_UI
# include "DistrhoUIInternal.hpp"
# include "../extra/RingBuffer.hpp"
#else
# include "../extra/Sleep.hpp"
#endif

#ifdef DPF_RUNTIME_TESTING
# include "../extra/Thread.hpp"
#endif

#if defined(HAVE_JACK) && defined(STATIC_BUILD) && !defined(DISTRHO_OS_WASM)
# define JACKBRIDGE_DIRECT
#endif

#include "jackbridge/JackBridge.cpp"
#include "lv2/lv2.h"

#ifdef DISTRHO_OS_MAC
# define Point CocoaPoint
# include <CoreFoundation/CoreFoundation.h>
# undef Point
#endif

#ifndef DISTRHO_OS_WINDOWS
# include <signal.h>
# include <unistd.h>
#endif

#ifdef __SSE2_MATH__
# include <xmmintrin.h>
#endif

#ifndef JACK_METADATA_ORDER
# define JACK_METADATA_ORDER "http://jackaudio.org/metadata/order"
#endif

#ifndef JACK_METADATA_PRETTY_NAME
# define JACK_METADATA_PRETTY_NAME "http://jackaudio.org/metadata/pretty-name"
#endif

#ifndef JACK_METADATA_PORT_GROUP
# define JACK_METADATA_PORT_GROUP "http://jackaudio.org/metadata/port-group"
#endif

#ifndef JACK_METADATA_SIGNAL_TYPE
# define JACK_METADATA_SIGNAL_TYPE "http://jackaudio.org/metadata/signal-type"
#endif

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

START_NAMESPACE_DISTRHO

#if DISTRHO_PLUGIN_HAS_UI && ! DISTRHO_PLUGIN_WANT_MIDI_INPUT
static const sendNoteFunc sendNoteCallback = nullptr;
#endif
#if DISTRHO_PLUGIN_HAS_UI && ! DISTRHO_PLUGIN_WANT_STATE
static const setStateFunc setStateCallback = nullptr;
#endif
#if ! DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
static const writeMidiFunc writeMidiCallback = nullptr;
#endif
#if ! DISTRHO_PLUGIN_WANT_PARAMETER_VALUE_CHANGE_REQUEST
static const requestParameterValueChangeFunc requestParameterValueChangeCallback = nullptr;
#endif

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

static volatile bool gCloseSignalReceived = false;

#ifdef DISTRHO_OS_WINDOWS
static BOOL WINAPI winSignalHandler(DWORD dwCtrlType) noexcept
{
    if (dwCtrlType == CTRL_C_EVENT)
    {
        gCloseSignalReceived = true;
        return TRUE;
    }
    return FALSE;
}

static void initSignalHandler()
{
    SetConsoleCtrlHandler(winSignalHandler, TRUE);
}
#else
static void closeSignalHandler(int) noexcept
{
    gCloseSignalReceived = true;
}

static void initSignalHandler()
{
    struct sigaction sig;
    memset(&sig, 0, sizeof(sig));

    sig.sa_handler = closeSignalHandler;
    sig.sa_flags   = SA_RESTART;
    sigemptyset(&sig.sa_mask);
    sigaction(SIGINT, &sig, nullptr);
    sigaction(SIGTERM, &sig, nullptr);
}
#endif

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

#if DISTRHO_PLUGIN_HAS_UI
class PluginJack : public DGL_NAMESPACE::IdleCallback
#else
class PluginJack
#endif
{
public:
    PluginJack(jack_client_t* const client, const uintptr_t winId)
        : fPlugin(this, writeMidiCallback, requestParameterValueChangeCallback, nullptr),
#if DISTRHO_PLUGIN_HAS_UI
          fUI(this,
              winId,
              d_nextSampleRate,
              nullptr, // edit param
              setParameterValueCallback,
              setStateCallback,
              sendNoteCallback,
              nullptr, // window size
              nullptr, // file request
              nullptr, // bundle
              fPlugin.getInstancePointer(),
              0.0),
#endif
          fClient(client)
    {
#if DISTRHO_PLUGIN_NUM_INPUTS > 0 || DISTRHO_PLUGIN_NUM_OUTPUTS > 0
        char strBuf[0xff+1];
        strBuf[0xff] = '\0';

# if DISTRHO_PLUGIN_NUM_INPUTS > 0
        for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS; ++i)
        {
            const AudioPort& port(fPlugin.getAudioPort(true, i));
            fPortAudioIns[i] = jackbridge_port_register(fClient, port.symbol, JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
            setAudioPortMetadata(port, fPortAudioIns[i], i);
        }
# endif
# if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
        for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS; ++i)
        {
            std::snprintf(strBuf, 0xff, "out%i", i+1);
            const AudioPort& port(fPlugin.getAudioPort(false, i));
            fPortAudioOuts[i] = jackbridge_port_register(fClient, port.symbol, JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
            setAudioPortMetadata(port, fPortAudioOuts[i], DISTRHO_PLUGIN_NUM_INPUTS+i);
        }
# endif
#endif

        fPortEventsIn = jackbridge_port_register(fClient, "events-in", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0);

#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
        fPortMidiOut = jackbridge_port_register(fClient, "midi-out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0);
        fPortMidiOutBuffer = nullptr;
#endif

#if DISTRHO_PLUGIN_WANT_PROGRAMS
        if (fPlugin.getProgramCount() > 0)
        {
            fPlugin.loadProgram(0);
# if DISTRHO_PLUGIN_HAS_UI
            fUI.programLoaded(0);
# endif
        }
# if DISTRHO_PLUGIN_HAS_UI
        fProgramChanged = -1;
# endif
#endif

        if (const uint32_t count = fPlugin.getParameterCount())
        {
            fLastOutputValues = new float[count];
            std::memset(fLastOutputValues, 0, sizeof(float)*count);

#if DISTRHO_PLUGIN_HAS_UI
            fParametersChanged = new bool[count];
            std::memset(fParametersChanged, 0, sizeof(bool)*count);
#endif

            for (uint32_t i=0; i < count; ++i)
            {
#if DISTRHO_PLUGIN_HAS_UI
                if (! fPlugin.isParameterOutput(i))
                    fUI.parameterChanged(i, fPlugin.getParameterValue(i));
#endif
            }
        }
        else
        {
            fLastOutputValues = nullptr;
#if DISTRHO_PLUGIN_HAS_UI
            fParametersChanged = nullptr;
#endif
        }

        jackbridge_set_thread_init_callback(fClient, jackThreadInitCallback, this);
        jackbridge_set_buffer_size_callback(fClient, jackBufferSizeCallback, this);
        jackbridge_set_sample_rate_callback(fClient, jackSampleRateCallback, this);
        jackbridge_set_process_callback(fClient, jackProcessCallback, this);
        jackbridge_on_shutdown(fClient, jackShutdownCallback, this);

        fPlugin.activate();

        jackbridge_activate(fClient);

        std::fflush(stdout);

#if DISTRHO_PLUGIN_HAS_UI
        if (const char* const name = jackbridge_get_client_name(fClient))
            fUI.setWindowTitle(name);
        else
            fUI.setWindowTitle(fPlugin.getName());

        fUI.exec(this);
#else
        while (! gCloseSignalReceived)
            d_sleep(1);

        // unused
        (void)winId;
#endif
    }

    ~PluginJack()
    {
        if (fClient != nullptr)
            jackbridge_deactivate(fClient);

        if (fLastOutputValues != nullptr)
        {
            delete[] fLastOutputValues;
            fLastOutputValues = nullptr;
        }

#if DISTRHO_PLUGIN_HAS_UI
        if (fParametersChanged != nullptr)
        {
            delete[] fParametersChanged;
            fParametersChanged = nullptr;
        }
#endif

        fPlugin.deactivate();

        if (fClient == nullptr)
            return;

#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
        jackbridge_port_unregister(fClient, fPortMidiOut);
        fPortMidiOut = nullptr;
#endif

        jackbridge_port_unregister(fClient, fPortEventsIn);
        fPortEventsIn = nullptr;

#if DISTRHO_PLUGIN_NUM_INPUTS > 0
        for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS; ++i)
        {
            jackbridge_port_unregister(fClient, fPortAudioIns[i]);
            fPortAudioIns[i] = nullptr;
        }
#endif

#if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
        for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS; ++i)
        {
            jackbridge_port_unregister(fClient, fPortAudioOuts[i]);
            fPortAudioOuts[i] = nullptr;
        }
#endif

        jackbridge_client_close(fClient);
    }

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

protected:
#if DISTRHO_PLUGIN_HAS_UI
    void idleCallback() override
    {
        if (gCloseSignalReceived)
            return fUI.quit();

# if DISTRHO_PLUGIN_WANT_PROGRAMS
        if (fProgramChanged >= 0)
        {
            fUI.programLoaded(fProgramChanged);
            fProgramChanged = -1;
        }
# endif

        for (uint32_t i=0, count=fPlugin.getParameterCount(); i < count; ++i)
        {
            if (fPlugin.isParameterOutput(i))
            {
                const float value = fPlugin.getParameterValue(i);

                if (d_isEqual(fLastOutputValues[i], value))
                    continue;

                fLastOutputValues[i] = value;
                fUI.parameterChanged(i, value);
            }
            else if (fParametersChanged[i])
            {
                fParametersChanged[i] = false;
                fUI.parameterChanged(i, fPlugin.getParameterValue(i));
            }
        }

        fUI.exec_idle();
    }
#endif

    void jackBufferSize(const jack_nframes_t nframes)
    {
        fPlugin.setBufferSize(nframes, true);
    }

    void jackSampleRate(const jack_nframes_t nframes)
    {
        fPlugin.setSampleRate(nframes, true);
    }

    void jackProcess(const jack_nframes_t nframes)
    {
#if DISTRHO_PLUGIN_NUM_INPUTS > 0
        const float* audioIns[DISTRHO_PLUGIN_NUM_INPUTS];

        for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_INPUTS; ++i)
            audioIns[i] = (const float*)jackbridge_port_get_buffer(fPortAudioIns[i], nframes);
#else
        static const float** audioIns = nullptr;
#endif

#if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
        float* audioOuts[DISTRHO_PLUGIN_NUM_OUTPUTS];

        for (uint32_t i=0; i < DISTRHO_PLUGIN_NUM_OUTPUTS; ++i)
            audioOuts[i] = (float*)jackbridge_port_get_buffer(fPortAudioOuts[i], nframes);
#else
        static float** audioOuts = nullptr;
#endif

#if DISTRHO_PLUGIN_WANT_TIMEPOS
        jack_position_t pos;
        fTimePosition.playing = (jackbridge_transport_query(fClient, &pos) == JackTransportRolling);

        if (pos.unique_1 == pos.unique_2)
        {
            fTimePosition.frame = pos.frame;

            if (pos.valid & JackPositionBBT)
            {
                fTimePosition.bbt.valid = true;

                fTimePosition.bbt.bar  = pos.bar;
                fTimePosition.bbt.beat = pos.beat;
                fTimePosition.bbt.tick = pos.tick;
#ifdef JACK_TICK_DOUBLE
                if (pos.valid & JackTickDouble)
                    fTimePosition.bbt.tick = pos.tick_double;
                else
#endif
                    fTimePosition.bbt.tick = pos.tick;
                fTimePosition.bbt.barStartTick = pos.bar_start_tick;

                fTimePosition.bbt.beatsPerBar = pos.beats_per_bar;
                fTimePosition.bbt.beatType    = pos.beat_type;

                fTimePosition.bbt.ticksPerBeat   = pos.ticks_per_beat;
                fTimePosition.bbt.beatsPerMinute = pos.beats_per_minute;
            }
            else
                fTimePosition.bbt.valid = false;
        }
        else
        {
            fTimePosition.bbt.valid = false;
            fTimePosition.frame = 0;
        }

        fPlugin.setTimePosition(fTimePosition);
#endif

        updateParameterTriggers();

#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
        fPortMidiOutBuffer = jackbridge_port_get_buffer(fPortMidiOut, nframes);
        jackbridge_midi_clear_buffer(fPortMidiOutBuffer);
#endif

#if DISTRHO_PLUGIN_WANT_MIDI_INPUT
        uint32_t  midiEventCount = 0;
        MidiEvent midiEvents[512];

# if DISTRHO_PLUGIN_HAS_UI
        while (fNotesRingBuffer.isDataAvailableForReading())
        {
            uint8_t midiData[3];
            if (! fNotesRingBuffer.readCustomData(midiData, 3))
                break;

            MidiEvent& midiEvent(midiEvents[midiEventCount++]);
            midiEvent.frame = 0;
            midiEvent.size  = 3;
            std::memcpy(midiEvent.data, midiData, 3);

            if (midiEventCount == 512)
                break;
        }
# endif
#else
        static const uint32_t midiEventCount = 0;
#endif

        void* const midiInBuf = jackbridge_port_get_buffer(fPortEventsIn, nframes);

        if (const uint32_t eventCount = std::min(512u - midiEventCount, jackbridge_midi_get_event_count(midiInBuf)))
        {
            jack_midi_event_t jevent;

            for (uint32_t i=0; i < eventCount; ++i)
            {
                if (! jackbridge_midi_event_get(&jevent, midiInBuf, i))
                    break;

                // Check if message is control change on channel 1
                if (jevent.buffer[0] == 0xB0 && jevent.size == 3)
                {
                    const uint8_t control = jevent.buffer[1];
                    const uint8_t value   = jevent.buffer[2];

                    /* NOTE: This is not optimal, we're iterating all parameters on every CC message.
                             Since the JACK standalone is more of a test tool, this will do for now. */
                    for (uint32_t j=0, paramCount=fPlugin.getParameterCount(); j < paramCount; ++j)
                    {
                        if (fPlugin.isParameterOutput(j))
                            continue;
                        if (fPlugin.getParameterMidiCC(j) != control)
                            continue;

                        const float scaled = static_cast<float>(value)/127.0f;
                        const float fvalue = fPlugin.getParameterRanges(j).getUnnormalizedValue(scaled);
                        fPlugin.setParameterValue(j, fvalue);
#if DISTRHO_PLUGIN_HAS_UI
                        fParametersChanged[j] = true;
#endif
                        break;
                    }
                }
#if DISTRHO_PLUGIN_WANT_PROGRAMS
                // Check if message is program change on channel 1
                else if (jevent.buffer[0] == 0xC0 && jevent.size == 2)
                {
                    const uint8_t program = jevent.buffer[1];

                    if (program < fPlugin.getProgramCount())
                    {
                        fPlugin.loadProgram(program);
# if DISTRHO_PLUGIN_HAS_UI
                        fProgramChanged = program;
# endif
                    }
                }
#endif

#if DISTRHO_PLUGIN_WANT_MIDI_INPUT
                MidiEvent& midiEvent(midiEvents[midiEventCount++]);

                midiEvent.frame = jevent.time;
                midiEvent.size  = static_cast<uint32_t>(jevent.size);

                if (midiEvent.size > MidiEvent::kDataSize)
                    midiEvent.dataExt = jevent.buffer;
                else
                    std::memcpy(midiEvent.data, jevent.buffer, midiEvent.size);
#endif
            }
        }

#if DISTRHO_PLUGIN_WANT_MIDI_INPUT
        fPlugin.run(audioIns, audioOuts, nframes, midiEvents, midiEventCount);
#else
        fPlugin.run(audioIns, audioOuts, nframes);
#endif

#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
        fPortMidiOutBuffer = nullptr;
#endif
    }

    void jackShutdown()
    {
        d_stderr("jack has shutdown, quitting now...");
        fClient = nullptr;
#if DISTRHO_PLUGIN_HAS_UI
        fUI.quit();
#endif
    }

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

#if DISTRHO_PLUGIN_HAS_UI
    void setParameterValue(const uint32_t index, const float value)
    {
        fPlugin.setParameterValue(index, value);
    }

# if DISTRHO_PLUGIN_WANT_MIDI_INPUT
    void sendNote(const uint8_t channel, const uint8_t note, const uint8_t velocity)
    {
        uint8_t midiData[3];
        midiData[0] = (velocity != 0 ? 0x90 : 0x80) | channel;
        midiData[1] = note;
        midiData[2] = velocity;
        fNotesRingBuffer.writeCustomData(midiData, 3);
        fNotesRingBuffer.commitWrite();
    }
# endif

# if DISTRHO_PLUGIN_WANT_STATE
    void setState(const char* const key, const char* const value)
    {
        fPlugin.setState(key, value);
    }
# endif
#endif // DISTRHO_PLUGIN_HAS_UI

    // NOTE: no trigger support for JACK, simulate it here
    void updateParameterTriggers()
    {
        float defValue;

        for (uint32_t i=0, count=fPlugin.getParameterCount(); i < count; ++i)
        {
            if ((fPlugin.getParameterHints(i) & kParameterIsTrigger) != kParameterIsTrigger)
                continue;

            defValue = fPlugin.getParameterRanges(i).def;

            if (d_isNotEqual(defValue, fPlugin.getParameterValue(i)))
                fPlugin.setParameterValue(i, defValue);
        }
    }

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

private:
    PluginExporter fPlugin;
#if DISTRHO_PLUGIN_HAS_UI
    UIExporter     fUI;
#endif

    jack_client_t* fClient;

#if DISTRHO_PLUGIN_NUM_INPUTS > 0
    jack_port_t* fPortAudioIns[DISTRHO_PLUGIN_NUM_INPUTS];
#endif
#if DISTRHO_PLUGIN_NUM_OUTPUTS > 0
    jack_port_t* fPortAudioOuts[DISTRHO_PLUGIN_NUM_OUTPUTS];
#endif
    jack_port_t* fPortEventsIn;
#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
    jack_port_t* fPortMidiOut;
    void*        fPortMidiOutBuffer;
#endif
#if DISTRHO_PLUGIN_WANT_TIMEPOS
    TimePosition fTimePosition;
#endif

    // Temporary data
    float* fLastOutputValues;

#if DISTRHO_PLUGIN_HAS_UI
    // Store DSP changes to send to UI
    bool* fParametersChanged;
# if DISTRHO_PLUGIN_WANT_PROGRAMS
    int fProgramChanged;
# endif
# if DISTRHO_PLUGIN_WANT_MIDI_INPUT
    SmallStackRingBuffer fNotesRingBuffer;
# endif
#endif

    void setAudioPortMetadata(const AudioPort& port, jack_port_t* const jackport, const uint32_t index)
    {
        DISTRHO_SAFE_ASSERT_RETURN(jackport != nullptr,);

        const jack_uuid_t uuid = jackbridge_port_uuid(jackport);

        if (uuid == JACK_UUID_EMPTY_INITIALIZER)
            return;

        jackbridge_set_property(fClient, uuid, JACK_METADATA_PRETTY_NAME, port.name, "text/plain");

        {
            char strBuf[0xff];
            snprintf(strBuf, sizeof(0xff)-1, "%u", index);
            jackbridge_set_property(fClient, uuid, JACK_METADATA_ORDER, strBuf, "http://www.w3.org/2001/XMLSchema#integer");
        }

        if (port.groupId != kPortGroupNone)
        {
            const PortGroupWithId& portGroup(fPlugin.getPortGroupById(port.groupId));
            jackbridge_set_property(fClient, uuid, JACK_METADATA_PORT_GROUP, portGroup.name, "text/plain");
        }

        if (port.hints & kAudioPortIsCV)
        {
            jackbridge_set_property(fClient, uuid, JACK_METADATA_SIGNAL_TYPE, "CV", "text/plain");
        }
        else
        {
            jackbridge_set_property(fClient, uuid, JACK_METADATA_SIGNAL_TYPE, "AUDIO", "text/plain");
            return;
        }

        // set cv ranges
        const bool cvPortScaled = port.hints & kCVPortHasScaledRange;

        if (port.hints & kCVPortHasBipolarRange)
        {
            if (cvPortScaled)
            {
                jackbridge_set_property(fClient, uuid, LV2_CORE__minimum, "-5", "http://www.w3.org/2001/XMLSchema#integer");
                jackbridge_set_property(fClient, uuid, LV2_CORE__maximum, "5", "http://www.w3.org/2001/XMLSchema#integer");
            }
            else
            {
                jackbridge_set_property(fClient, uuid, LV2_CORE__minimum, "-1", "http://www.w3.org/2001/XMLSchema#integer");
                jackbridge_set_property(fClient, uuid, LV2_CORE__maximum, "1", "http://www.w3.org/2001/XMLSchema#integer");
            }
        }
        else if (port.hints & kCVPortHasNegativeUnipolarRange)
        {
            if (cvPortScaled)
            {
                jackbridge_set_property(fClient, uuid, LV2_CORE__minimum, "-10", "http://www.w3.org/2001/XMLSchema#integer");
                jackbridge_set_property(fClient, uuid, LV2_CORE__maximum, "0", "http://www.w3.org/2001/XMLSchema#integer");
            }
            else
            {
                jackbridge_set_property(fClient, uuid, LV2_CORE__minimum, "-1", "http://www.w3.org/2001/XMLSchema#integer");
                jackbridge_set_property(fClient, uuid, LV2_CORE__maximum, "0", "http://www.w3.org/2001/XMLSchema#integer");
            }
        }
        else if (port.hints & kCVPortHasPositiveUnipolarRange)
        {
            if (cvPortScaled)
            {
                jackbridge_set_property(fClient, uuid, LV2_CORE__minimum, "0", "http://www.w3.org/2001/XMLSchema#integer");
                jackbridge_set_property(fClient, uuid, LV2_CORE__maximum, "10", "http://www.w3.org/2001/XMLSchema#integer");
            }
            else
            {
                jackbridge_set_property(fClient, uuid, LV2_CORE__minimum, "0", "http://www.w3.org/2001/XMLSchema#integer");
                jackbridge_set_property(fClient, uuid, LV2_CORE__maximum, "1", "http://www.w3.org/2001/XMLSchema#integer");
            }
        }
    }

    // -------------------------------------------------------------------
    // Callbacks

    #define thisPtr ((PluginJack*)ptr)

    static void jackThreadInitCallback(void*)
    {
       #if defined(__SSE2_MATH__)
        _mm_setcsr(_mm_getcsr() | 0x8040);
       #elif defined(__aarch64__)
        uint64_t c;
        __asm__ __volatile__("mrs %0, fpcr          \n"
                             "orr %0, %0, #0x1000000\n"
                             "msr fpcr, %0          \n"
                             "isb                   \n"
                             : "=r"(c) :: "memory");
       #elif defined(__arm__)
        uint32_t c;
        __asm__ __volatile__("vmrs %0, fpscr         \n"
                             "orr  %0, %0, #0x1000000\n"
                             "vmsr fpscr, %0         \n"
                             : "=r"(c) :: "memory");
       #endif
    }

    static int jackBufferSizeCallback(jack_nframes_t nframes, void* ptr)
    {
        thisPtr->jackBufferSize(nframes);
        return 0;
    }

    static int jackSampleRateCallback(jack_nframes_t nframes, void* ptr)
    {
        thisPtr->jackSampleRate(nframes);
        return 0;
    }

    static int jackProcessCallback(jack_nframes_t nframes, void* ptr)
    {
        thisPtr->jackProcess(nframes);
        return 0;
    }

    static void jackShutdownCallback(void* ptr)
    {
        thisPtr->jackShutdown();
    }

#if DISTRHO_PLUGIN_HAS_UI
    static void setParameterValueCallback(void* ptr, uint32_t index, float value)
    {
        thisPtr->setParameterValue(index, value);
    }

# if DISTRHO_PLUGIN_WANT_MIDI_INPUT
    static void sendNoteCallback(void* ptr, uint8_t channel, uint8_t note, uint8_t velocity)
    {
        thisPtr->sendNote(channel, note, velocity);
    }
# endif

# if DISTRHO_PLUGIN_WANT_STATE
    static void setStateCallback(void* ptr, const char* key, const char* value)
    {
        thisPtr->setState(key, value);
    }
# endif
#endif // DISTRHO_PLUGIN_HAS_UI

#if DISTRHO_PLUGIN_WANT_PARAMETER_VALUE_CHANGE_REQUEST
    bool requestParameterValueChange(const uint32_t index, const float value)
    {
        DISTRHO_SAFE_ASSERT_RETURN(index < fPlugin.getParameterCount(), false);

        fPlugin.setParameterValue(index, value);
# if DISTRHO_PLUGIN_HAS_UI
        fParametersChanged[index] = true;
# endif
        return true;
    }

    static bool requestParameterValueChangeCallback(void* ptr, const uint32_t index, const float value)
    {
        return thisPtr->requestParameterValueChange(index, value);
    }
#endif

#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
    bool writeMidi(const MidiEvent& midiEvent)
    {
        DISTRHO_SAFE_ASSERT_RETURN(fPortMidiOutBuffer != nullptr, false);

        return jackbridge_midi_event_write(fPortMidiOutBuffer,
                                           midiEvent.frame,
                                           midiEvent.size > MidiEvent::kDataSize ? midiEvent.dataExt : midiEvent.data,
                                           midiEvent.size);
    }

    static bool writeMidiCallback(void* ptr, const MidiEvent& midiEvent)
    {
        return thisPtr->writeMidi(midiEvent);
    }
#endif

    #undef thisPtr
};

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

#ifdef DPF_RUNTIME_TESTING
class PluginProcessTestingThread : public Thread
{
    PluginExporter& plugin;

public:
    PluginProcessTestingThread(PluginExporter& p) : plugin(p) {}

protected:
    void run() override
    {
        plugin.setBufferSize(256);
        plugin.activate();

        float buffer[256];
        const float* inputs[DISTRHO_PLUGIN_NUM_INPUTS > 0 ? DISTRHO_PLUGIN_NUM_INPUTS : 1];
        float* outputs[DISTRHO_PLUGIN_NUM_OUTPUTS > 0 ? DISTRHO_PLUGIN_NUM_OUTPUTS : 1];
        for (int i=0; i<DISTRHO_PLUGIN_NUM_INPUTS; ++i)
            inputs[i] = buffer;
        for (int i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS; ++i)
            outputs[i] = buffer;

        while (! shouldThreadExit())
        {
           #if DISTRHO_PLUGIN_WANT_MIDI_INPUT
            plugin.run(inputs, outputs, 128, nullptr, 0);
           #else
            plugin.run(inputs, outputs, 128);
           #endif
            d_msleep(100);
        }

        plugin.deactivate();
    }
};

bool runSelfTests()
{
    // simple plugin creation first
    {
        d_nextBufferSize = 512;
        d_nextSampleRate = 44100.0;
        PluginExporter plugin(nullptr, nullptr, nullptr, nullptr);
        d_nextBufferSize = 0;
        d_nextSampleRate = 0.0;
    }

    // keep values for all tests now
    d_nextBufferSize = 512;
    d_nextSampleRate = 44100.0;

    // simple processing
    {
        d_nextPluginIsSelfTest = true;
        PluginExporter plugin(nullptr, nullptr, nullptr, nullptr);
        d_nextPluginIsSelfTest = false;

       #if DISTRHO_PLUGIN_HAS_UI
        UIExporter ui(nullptr, 0, plugin.getSampleRate(),
                      nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
                      plugin.getInstancePointer(), 0.0);
        ui.showAndFocus();
       #endif

        plugin.activate();
        plugin.deactivate();
        plugin.setBufferSize(128);
        plugin.setSampleRate(48000);
        plugin.activate();

        float buffer[128] = {};
        const float* inputs[DISTRHO_PLUGIN_NUM_INPUTS > 0 ? DISTRHO_PLUGIN_NUM_INPUTS : 1];
        float* outputs[DISTRHO_PLUGIN_NUM_OUTPUTS > 0 ? DISTRHO_PLUGIN_NUM_OUTPUTS : 1];
        for (int i=0; i<DISTRHO_PLUGIN_NUM_INPUTS; ++i)
            inputs[i] = buffer;
        for (int i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS; ++i)
            outputs[i] = buffer;

       #if DISTRHO_PLUGIN_WANT_MIDI_INPUT
        plugin.run(inputs, outputs, 128, nullptr, 0);
       #else
        plugin.run(inputs, outputs, 128);
       #endif

        plugin.deactivate();

       #if DISTRHO_PLUGIN_HAS_UI
        ui.plugin_idle();
       #endif
    }

    return true;

    // multi-threaded processing with UI
    {
        PluginExporter pluginA(nullptr, nullptr, nullptr, nullptr);
        PluginExporter pluginB(nullptr, nullptr, nullptr, nullptr);
        PluginExporter pluginC(nullptr, nullptr, nullptr, nullptr);
        PluginProcessTestingThread procTestA(pluginA);
        PluginProcessTestingThread procTestB(pluginB);
        PluginProcessTestingThread procTestC(pluginC);
        procTestA.startThread();
        procTestB.startThread();
        procTestC.startThread();

        // wait 2s
        d_sleep(2);

        // stop the 2nd instance now
        procTestB.stopThread(5000);

       #if DISTRHO_PLUGIN_HAS_UI
        // start UI in the middle of this
        {
            UIExporter uiA(nullptr, 0, pluginA.getSampleRate(),
                           nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
                           pluginA.getInstancePointer(), 0.0);
            UIExporter uiB(nullptr, 0, pluginA.getSampleRate(),
                           nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
                           pluginB.getInstancePointer(), 0.0);
            UIExporter uiC(nullptr, 0, pluginA.getSampleRate(),
                           nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
                           pluginC.getInstancePointer(), 0.0);

            // show UIs
            uiB.showAndFocus();
            uiA.showAndFocus();
            uiC.showAndFocus();

            // idle for 3s
            for (int i=0; i<30; i++)
            {
                uiC.plugin_idle();
                uiB.plugin_idle();
                uiA.plugin_idle();
                d_msleep(100);
            }
        }
       #endif

        procTestA.stopThread(5000);
        procTestC.stopThread(5000);
    }

    return true;
}
#endif // DPF_RUNTIME_TESTING

END_NAMESPACE_DISTRHO

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

int main(int argc, char* argv[])
{
    USE_NAMESPACE_DISTRHO;

    initSignalHandler();

   #if !defined(DISTRHO_OS_WINDOWS) && !defined(STATIC_BUILD)
    // find plugin bundle
    static String bundlePath;
    if (bundlePath.isEmpty())
    {
        String tmpPath(getBinaryFilename());
        tmpPath.truncate(tmpPath.rfind(DISTRHO_OS_SEP));
       #ifdef DISTRHO_OS_MAC
        if (tmpPath.endsWith("/MacOS"))
        {
            tmpPath.truncate(tmpPath.rfind('/'));
            if (tmpPath.endsWith("/Contents"))
            {
                tmpPath.truncate(tmpPath.rfind('/'));
                bundlePath = tmpPath;
                d_nextBundlePath = bundlePath.buffer();
            }
        }
       #else
        if (access(tmpPath + DISTRHO_OS_SEP_STR "resources", F_OK) == 0)
        {
            bundlePath = tmpPath;
            d_nextBundlePath = bundlePath.buffer();
        }
       #endif
    }
   #endif

    if (argc == 2 && std::strcmp(argv[1], "selftest") == 0)
    {
       #ifdef DPF_RUNTIME_TESTING
        return runSelfTests() ? 0 : 1;
       #else
        d_stderr2("Code was built without DPF_RUNTIME_TESTING macro enabled, selftest option is not available");
        return 1;
       #endif
    }

   #if defined(DISTRHO_OS_WINDOWS) && DISTRHO_PLUGIN_HAS_UI
    /* the code below is based on
     * https://www.tillett.info/2013/05/13/how-to-create-a-windows-program-that-works-as-both-as-a-gui-and-console-application/
     */
    bool hasConsole = false;

    HANDLE consoleHandleOut, consoleHandleError;

    if (AttachConsole(ATTACH_PARENT_PROCESS))
    {
        // Redirect unbuffered STDOUT to the console
        consoleHandleOut = GetStdHandle(STD_OUTPUT_HANDLE);
        if (consoleHandleOut != INVALID_HANDLE_VALUE)
        {
            freopen("CONOUT$", "w", stdout);
            setvbuf(stdout, NULL, _IONBF, 0);
        }

        // Redirect unbuffered STDERR to the console
        consoleHandleError = GetStdHandle(STD_ERROR_HANDLE);
        if (consoleHandleError != INVALID_HANDLE_VALUE)
        {
            freopen("CONOUT$", "w", stderr);
            setvbuf(stderr, NULL, _IONBF, 0);
        }

        hasConsole = true;
    }
   #endif

    jack_status_t  status = jack_status_t(0x0);
    jack_client_t* client = jackbridge_client_open(DISTRHO_PLUGIN_NAME, JackNoStartServer, &status);

    if (client == nullptr)
    {
        String errorString;

        if (status & JackFailure)
            errorString += "Overall operation failed;\n";
        if (status & JackInvalidOption)
            errorString += "The operation contained an invalid or unsupported option;\n";
        if (status & JackNameNotUnique)
            errorString += "The desired client name was not unique;\n";
        if (status & JackServerStarted)
            errorString += "The JACK server was started as a result of this operation;\n";
        if (status & JackServerFailed)
            errorString += "Unable to connect to the JACK server;\n";
        if (status & JackServerError)
            errorString += "Communication error with the JACK server;\n";
        if (status & JackNoSuchClient)
            errorString += "Requested client does not exist;\n";
        if (status & JackLoadFailure)
            errorString += "Unable to load internal client;\n";
        if (status & JackInitFailure)
            errorString += "Unable to initialize client;\n";
        if (status & JackShmFailure)
            errorString += "Unable to access shared memory;\n";
        if (status & JackVersionError)
            errorString += "Client's protocol version does not match;\n";
        if (status & JackBackendError)
            errorString += "Backend Error;\n";
        if (status & JackClientZombie)
            errorString += "Client is being shutdown against its will;\n";

        if (errorString.isNotEmpty())
        {
            errorString[errorString.length()-2] = '.';
            d_stderr("Failed to create the JACK client, reason was:\n%s", errorString.buffer());
        }
        else
            d_stderr("Failed to create the JACK client, cannot continue!");

       #if defined(DISTRHO_OS_MAC)
        CFStringRef errorTitleRef = CFStringCreateWithCString(nullptr,
           DISTRHO_PLUGIN_NAME ": Error", kCFStringEncodingUTF8);
        CFStringRef errorStringRef = CFStringCreateWithCString(nullptr,
           String("Failed to create JACK client, reason was:\n" + errorString).buffer(), kCFStringEncodingUTF8);

        CFUserNotificationDisplayAlert(0, kCFUserNotificationCautionAlertLevel,
           nullptr, nullptr, nullptr,
           errorTitleRef, errorStringRef,
           nullptr, nullptr, nullptr, nullptr);
       #elif defined(DISTRHO_OS_WINDOWS) && DISTRHO_PLUGIN_HAS_UI
        // make sure message box is high-dpi aware
        if (const HMODULE user32 = LoadLibrary("user32.dll"))
        {
            typedef BOOL(WINAPI* SPDA)(void);
           #if defined(__GNUC__) && (__GNUC__ >= 9)
           # pragma GCC diagnostic push
           # pragma GCC diagnostic ignored "-Wcast-function-type"
           #endif
            const SPDA SetProcessDPIAware = (SPDA)GetProcAddress(user32, "SetProcessDPIAware");
           #if defined(__GNUC__) && (__GNUC__ >= 9)
           # pragma GCC diagnostic pop
           #endif
            if (SetProcessDPIAware)
                SetProcessDPIAware();
            FreeLibrary(user32);
        }

        const String win32error = "Failed to create JACK client, reason was:\n" + errorString;
        MessageBoxA(nullptr, win32error.buffer(), "", MB_ICONERROR);
       #endif

        return 1;
    }

    d_nextBufferSize = jackbridge_get_buffer_size(client);
    d_nextSampleRate = jackbridge_get_sample_rate(client);
    d_nextCanRequestParameterValueChanges = true;

    uintptr_t winId = 0;
   #if DISTRHO_PLUGIN_HAS_UI
    if (argc == 3 && std::strcmp(argv[1], "embed") == 0)
        winId = static_cast<uintptr_t>(std::atoll(argv[2]));
   #endif

    const PluginJack p(client, winId);

   #if defined(DISTRHO_OS_WINDOWS) && DISTRHO_PLUGIN_HAS_UI
    /* the code below is based on
     * https://www.tillett.info/2013/05/13/how-to-create-a-windows-program-that-works-as-both-as-a-gui-and-console-application/
     */

    // Send "enter" to release application from the console
    // This is a hack, but if not used the console doesn't know the application has
    // returned. The "enter" key only sent if the console window is in focus.
    if (hasConsole && (GetConsoleWindow() == GetForegroundWindow() || SetFocus(GetConsoleWindow()) != nullptr))
    {
        INPUT ip;
        // Set up a generic keyboard event.
        ip.type = INPUT_KEYBOARD;
        ip.ki.wScan = 0; // hardware scan code for key
        ip.ki.time = 0;
        ip.ki.dwExtraInfo = 0;

        // Send the "Enter" key
        ip.ki.wVk = 0x0D; // virtual-key code for the "Enter" key
        ip.ki.dwFlags = 0; // 0 for key press
        SendInput(1, &ip, sizeof(INPUT));

        // Release the "Enter" key
        ip.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release
        SendInput(1, &ip, sizeof(INPUT));
    }
   #endif

    return 0;
}

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