/*
 * DISTRHO Ildaeil Plugin
 * Copyright (C) 2021-2025 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 LICENSE file.
 */

#include "IldaeilBasePlugin.hpp"
#include "DistrhoPluginUtils.hpp"

#include "CarlaBackendUtils.hpp"
#include "CarlaEngine.hpp"
#include "water/files/File.h"
#include "water/streams/MemoryOutputStream.h"
#include "water/xml/XmlDocument.h"

START_NAMESPACE_DISTRHO

using namespace CARLA_BACKEND_NAMESPACE;

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

static uint32_t host_get_buffer_size(NativeHostHandle);
static double host_get_sample_rate(NativeHostHandle);
static bool host_is_offline(NativeHostHandle);
static const NativeTimeInfo* host_get_time_info(NativeHostHandle handle);
static bool host_write_midi_event(NativeHostHandle handle, const NativeMidiEvent* event);
static void host_ui_parameter_changed(NativeHostHandle handle, uint32_t index, float value);
static void host_ui_midi_program_changed(NativeHostHandle handle, uint8_t channel, uint32_t bank, uint32_t program);
static void host_ui_custom_data_changed(NativeHostHandle handle, const char* key, const char* value);
static void host_ui_closed(NativeHostHandle handle);
static const char* host_ui_open_file(NativeHostHandle handle, bool isDir, const char* title, const char* filter);
static const char* host_ui_save_file(NativeHostHandle handle, bool isDir, const char* title, const char* filter);
static intptr_t host_dispatcher(NativeHostHandle h, NativeHostDispatcherOpcode op, int32_t, intptr_t, void*, float);

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

Mutex IldaeilBasePlugin::sPluginInfoLoadMutex;

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

#ifndef CARLA_OS_WIN
static water::String getHomePath()
{
    static water::String path(water::File::getSpecialLocation(water::File::userHomeDirectory).getFullPathName());
    return path;
}
#endif

static const char* getPathForLADSPA()
{
    static water::String path;

    if (path.isEmpty())
    {
       #if defined(CARLA_OS_HAIKU)
        path = getHomePath() + "/.ladspa:/system/add-ons/media/ladspaplugins:/system/lib/ladspa";
       #elif defined(CARLA_OS_MAC)
        path = getHomePath() + "/Library/Audio/Plug-Ins/LADSPA:/Library/Audio/Plug-Ins/LADSPA";
       #elif defined(CARLA_OS_WASM)
        path = "/ladspa";
       #elif defined(CARLA_OS_WIN)
        path  = water::File::getSpecialLocation(water::File::winAppData).getFullPathName() + "\\LADSPA;";
        path += water::File::getSpecialLocation(water::File::winProgramFiles).getFullPathName() + "\\LADSPA";
       #else
        path  = getHomePath() + "/.ladspa:/usr/lib/ladspa:/usr/local/lib/ladspa";
       #endif
    }

    return path.toRawUTF8();
}

static const char* getPathForDSSI()
{
    static water::String path;

    if (path.isEmpty())
    {
       #if defined(CARLA_OS_HAIKU)
        path = getHomePath() + "/.dssi:/system/add-ons/media/dssiplugins:/system/lib/dssi";
       #elif defined(CARLA_OS_MAC)
        path = getHomePath() + "/Library/Audio/Plug-Ins/DSSI:/Library/Audio/Plug-Ins/DSSI";
       #elif defined(CARLA_OS_WASM)
        path = "/dssi";
       #elif defined(CARLA_OS_WIN)
        path  = water::File::getSpecialLocation(water::File::winAppData).getFullPathName() + "\\DSSI;";
        path += water::File::getSpecialLocation(water::File::winProgramFiles).getFullPathName() + "\\DSSI";
       #else
        path = getHomePath() + "/.dssi:/usr/lib/dssi:/usr/local/lib/dssi";
       #endif
    }

    return path.toRawUTF8();
}

static const char* getPathForLV2()
{
    static water::String path;

    if (path.isEmpty())
    {
       #if defined(CARLA_OS_HAIKU)
        path = getHomePath() + "/.lv2:/system/add-ons/media/lv2plugins";
       #elif defined(CARLA_OS_MAC)
        path = getHomePath() + "/Library/Audio/Plug-Ins/LV2:/Library/Audio/Plug-Ins/LV2";
       #elif defined(CARLA_OS_WASM)
        path = "/lv2";
       #elif defined(CARLA_OS_WIN)
        path  = water::File::getSpecialLocation(water::File::winAppData).getFullPathName() + "\\LV2;";
        path += water::File::getSpecialLocation(water::File::winCommonProgramFiles).getFullPathName() + "\\LV2";
       #else
        path = getHomePath() + "/.lv2:/usr/lib/lv2:/usr/local/lib/lv2";
       #endif
    }

    return path.toRawUTF8();
}

static const char* getPathForVST2()
{
    static water::String path;

    if (path.isEmpty())
    {
       #if defined(CARLA_OS_HAIKU)
        path = getHomePath() + "/.vst:/system/add-ons/media/vstplugins";
       #elif defined(CARLA_OS_MAC)
        path = getHomePath() + "/Library/Audio/Plug-Ins/VST:/Library/Audio/Plug-Ins/VST";
       #elif defined(CARLA_OS_WASM)
        path = "/vst";
       #elif defined(CARLA_OS_WIN)
        path  = water::File::getSpecialLocation(water::File::winProgramFiles).getFullPathName() + "\\VstPlugins;";
        path += water::File::getSpecialLocation(water::File::winProgramFiles).getFullPathName() + "\\Steinberg\\VstPlugins;";
        path += water::File::getSpecialLocation(water::File::winCommonProgramFiles).getFullPathName() + "\\VST2";
       #else
        path = getHomePath() + "/.vst:/usr/lib/vst:/usr/local/lib/vst";

        water::String winePrefix;
        if (const char* const envWINEPREFIX = std::getenv("WINEPREFIX"))
            winePrefix = envWINEPREFIX;

        if (winePrefix.isEmpty())
            winePrefix = getHomePath() + "/.wine";

        if (water::File(winePrefix.toRawUTF8()).exists())
        {
            path += ":" + winePrefix + "/drive_c/Program Files/Common Files/VST2";
            path += ":" + winePrefix + "/drive_c/Program Files/VstPlugins";
            path += ":" + winePrefix + "/drive_c/Program Files/VSTPlugins";
            path += ":" + winePrefix + "/drive_c/Program Files/Steinberg/VstPlugins";
            path += ":" + winePrefix + "/drive_c/Program Files/Steinberg/VSTPlugins";
           #ifdef CARLA_OS_64BIT
            path += ":" + winePrefix + "/drive_c/Program Files (x86)/Common Files/VST2";
            path += ":" + winePrefix + "/drive_c/Program Files (x86)/VstPlugins";
            path += ":" + winePrefix + "/drive_c/Program Files (x86)/VSTPlugins";
            path += ":" + winePrefix + "/drive_c/Program Files (x86)/Steinberg/VstPlugins";
            path += ":" + winePrefix + "/drive_c/Program Files (x86)/Steinberg/VSTPlugins";
           #endif
        }
       #endif
    }

    return path.toRawUTF8();
}

static const char* getPathForVST3()
{
    static water::String path;

    if (path.isEmpty())
    {
       #if defined(CARLA_OS_HAIKU)
        path = getHomePath() + "/.vst3:/system/add-ons/media/vst3plugins";
       #elif defined(CARLA_OS_MAC)
        path = getHomePath() + "/Library/Audio/Plug-Ins/VST3:/Library/Audio/Plug-Ins/VST3";
       #elif defined(CARLA_OS_WASM)
        path = "/vst3";
       #elif defined(CARLA_OS_WIN)
        path  = water::File::getSpecialLocation(water::File::winAppData).getFullPathName() + "\\VST3;";
        path += water::File::getSpecialLocation(water::File::winCommonProgramFiles).getFullPathName() + "\\VST3";
       #else
        path = getHomePath() + "/.vst3:/usr/lib/vst3:/usr/local/lib/vst3";

        water::String winePrefix;
        if (const char* const envWINEPREFIX = std::getenv("WINEPREFIX"))
            winePrefix = envWINEPREFIX;

        if (winePrefix.isEmpty())
            winePrefix = getHomePath() + "/.wine";

        if (water::File(winePrefix.toRawUTF8()).exists())
        {
            path += ":" + winePrefix + "/drive_c/Program Files/Common Files/VST3";
           #ifdef CARLA_OS_64BIT
            path += ":" + winePrefix + "/drive_c/Program Files (x86)/Common Files/VST3";
           #endif
        }
       #endif
    }

    return path.toRawUTF8();
}

static const char* getPathForCLAP()
{
    static water::String path;

    if (path.isEmpty())
    {
       #if defined(CARLA_OS_HAIKU)
        path = getHomePath() + "/.clap:/system/add-ons/media/clapplugins";
       #elif defined(CARLA_OS_MAC)
        path = getHomePath() + "/Library/Audio/Plug-Ins/CLAP:/Library/Audio/Plug-Ins/CLAP";
       #elif defined(CARLA_OS_WASM)
        path = "/clap";
       #elif defined(CARLA_OS_WIN)
        path  = water::File::getSpecialLocation(water::File::winAppData).getFullPathName() + "\\CLAP;";
        path += water::File::getSpecialLocation(water::File::winCommonProgramFiles).getFullPathName() + "\\CLAP";
       #else
        path = getHomePath() + "/.clap:/usr/lib/clap:/usr/local/lib/clap";

        water::String winePrefix;
        if (const char* const envWINEPREFIX = std::getenv("WINEPREFIX"))
            winePrefix = envWINEPREFIX;

        if (winePrefix.isEmpty())
            winePrefix = getHomePath() + "/.wine";

        if (water::File(winePrefix.toRawUTF8()).exists())
        {
            path += ":" + winePrefix + "/drive_c/Program Files/Common Files/CLAP";
           #ifdef CARLA_OS_64BIT
            path += ":" + winePrefix + "/drive_c/Program Files (x86)/Common Files/CLAP";
           #endif
        }
       #endif
    }

    return path.toRawUTF8();
}

static const char* getPathForJSFX()
{
    static water::String path;

    if (path.isEmpty())
    {
       #if defined(CARLA_OS_MAC)
        path = getHomePath()
             + "/Library/Application Support/REAPER/Effects";
        if (! water::File(path.toRawUTF8()).isDirectory())
            path = "/Applications/REAPER.app/Contents/InstallFiles/Effects";
       #elif defined(CARLA_OS_WASM)
        path = "/jsfx";
       #elif defined(CARLA_OS_WIN)
        path = water::File::getSpecialLocation(water::File::winAppData).getFullPathName() + "\\REAPER\\Effects";
        if (! water::File(path.toRawUTF8()).isDirectory())
            path = water::File::getSpecialLocation(water::File::winProgramFiles).getFullPathName()
                 + "\\REAPER\\InstallData\\Effects";
       #else
        if (const char* const configHome = std::getenv("XDG_CONFIG_HOME"))
            path = configHome;
        else
            path = getHomePath() + "/.config";
        path += "/REAPER/Effects";
       #endif
    }

    return path.toRawUTF8();
}

const char* IldaeilBasePlugin::getPluginPath(const PluginType ptype)
{
    switch (ptype)
    {
    case PLUGIN_LADSPA:
        if (const char* const path = std::getenv("LADSPA_PATH"))
            return path;
        return getPathForLADSPA();
    case PLUGIN_DSSI:
        if (const char* const path = std::getenv("DSSI_PATH"))
            return path;
        return getPathForDSSI();
    case PLUGIN_LV2:
        if (const char* const path = std::getenv("LV2_PATH"))
            return path;
        return getPathForLV2();
    case PLUGIN_VST2:
        if (const char* const path = std::getenv("VST_PATH"))
            return path;
        return getPathForVST2();
    case PLUGIN_VST3:
        if (const char* const path = std::getenv("VST3_PATH"))
            return path;
        return getPathForVST3();
    case PLUGIN_CLAP:
        if (const char* const path = std::getenv("CLAP_PATH"))
            return path;
        return getPathForCLAP();
    case PLUGIN_JSFX:
        return getPathForJSFX();
    default:
        return nullptr;
    }
}

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

class IldaeilPlugin : public IldaeilBasePlugin
{
   #if DISTRHO_PLUGIN_NUM_INPUTS == 0 || DISTRHO_PLUGIN_NUM_OUTPUTS == 0
    float* fDummyBuffer = nullptr;
    float* fDummyBuffers[2];
   #endif
   #if DISTRHO_PLUGIN_WANT_MIDI_INPUT
    static constexpr const uint kMaxMidiEventCount = 512;
    NativeMidiEvent* fMidiEvents = nullptr;
   #endif

    mutable NativeTimeInfo fCarlaTimeInfo{};
    mutable water::MemoryOutputStream fLastProjectState;
    uint32_t fLastLatencyValue = 0;

public:
    IldaeilPlugin()
        : IldaeilBasePlugin()
    {
        fCarlaPluginDescriptor = carla_get_native_rack_plugin();
        DISTRHO_SAFE_ASSERT_RETURN(fCarlaPluginDescriptor != nullptr,);

        fCarlaHostDescriptor.handle = this;
        fCarlaHostDescriptor.resourceDir = carla_get_library_folder();
        fCarlaHostDescriptor.uiName = "Ildaeil";
        fCarlaHostDescriptor.uiParentId = 0;

        fCarlaHostDescriptor.get_buffer_size = host_get_buffer_size;
        fCarlaHostDescriptor.get_sample_rate = host_get_sample_rate;
        fCarlaHostDescriptor.is_offline = host_is_offline;

        fCarlaHostDescriptor.get_time_info = host_get_time_info;
        fCarlaHostDescriptor.write_midi_event = host_write_midi_event;
        fCarlaHostDescriptor.ui_parameter_changed = host_ui_parameter_changed;
        fCarlaHostDescriptor.ui_midi_program_changed = host_ui_midi_program_changed;
        fCarlaHostDescriptor.ui_custom_data_changed = host_ui_custom_data_changed;
        fCarlaHostDescriptor.ui_closed = host_ui_closed;
        fCarlaHostDescriptor.ui_open_file = host_ui_open_file;
        fCarlaHostDescriptor.ui_save_file = host_ui_save_file;
        fCarlaHostDescriptor.dispatcher = host_dispatcher;

        fCarlaPluginHandle = fCarlaPluginDescriptor->instantiate(&fCarlaHostDescriptor);
        DISTRHO_SAFE_ASSERT_RETURN(fCarlaPluginHandle != nullptr,);

        fCarlaHostHandle = carla_create_native_plugin_host_handle(fCarlaPluginDescriptor, fCarlaPluginHandle);
        DISTRHO_SAFE_ASSERT_RETURN(fCarlaHostHandle != nullptr,);

        const char* const bundlePath = getBundlePath();
       #ifdef CARLA_OS_WIN
        #define EXT ".exe"
       #else
        #define EXT ""
       #endif

        if (bundlePath != nullptr
            && water::File(bundlePath + String(DISTRHO_OS_SEP_STR "carla-bridge-native" EXT)).existsAsFile())
        {
            fBinaryPath = bundlePath;
            carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_BINARIES, 0, bundlePath);
            carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_RESOURCES, 0, getResourcePath(bundlePath));
        }
       #ifdef CARLA_OS_MAC
        else if (bundlePath != nullptr
            && water::File(bundlePath + String("/Contents/MacOS/carla-bridge-native" EXT)).existsAsFile())
        {
            fBinaryPath = bundlePath;
            fBinaryPath += "/Contents/MacOS";
            carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_BINARIES, 0, fBinaryPath);
            carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_RESOURCES, 0, getResourcePath(bundlePath));
        }
       #endif
        else
        {
           #ifdef CARLA_OS_MAC
            fBinaryPath = "/Applications/Carla.app/Contents/MacOS";
            carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_BINARIES, 0, "/Applications/Carla.app/Contents/MacOS");
            carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_RESOURCES, 0, "/Applications/Carla.app/Contents/MacOS/resources");
           #else
            fBinaryPath = "/usr/lib/carla";
            carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_BINARIES, 0, "/usr/lib/carla");
            carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PATH_RESOURCES, 0, "/usr/share/carla/resources");
           #endif
        }

        if (fBinaryPath.isNotEmpty())
            carla_stdout("Using binary path for discovery tools: %s", fBinaryPath.buffer());

        #undef EXT

        carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, PLUGIN_LADSPA, getPluginPath(PLUGIN_LADSPA));
        carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, PLUGIN_DSSI, getPluginPath(PLUGIN_DSSI));
        carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, PLUGIN_LV2, getPluginPath(PLUGIN_LV2));
        carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, PLUGIN_VST2, getPluginPath(PLUGIN_VST2));
        carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, PLUGIN_VST3, getPluginPath(PLUGIN_VST3));
        carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, PLUGIN_CLAP, getPluginPath(PLUGIN_CLAP));
        carla_set_engine_option(fCarlaHostHandle, ENGINE_OPTION_PLUGIN_PATH, PLUGIN_JSFX, getPluginPath(PLUGIN_JSFX));

        fCarlaPluginDescriptor->dispatcher(fCarlaPluginHandle, NATIVE_PLUGIN_OPCODE_HOST_USES_EMBED,
                                           0, 0, nullptr, 0.0f);

       #if DISTRHO_PLUGIN_WANT_MIDI_INPUT
        fMidiEvents = new NativeMidiEvent[kMaxMidiEventCount];
       #endif

       #if DISTRHO_PLUGIN_NUM_INPUTS == 0 || DISTRHO_PLUGIN_NUM_OUTPUTS == 0
        // create dummy buffers
        bufferSizeChanged(getBufferSize());
       #endif
    }

    ~IldaeilPlugin() override
    {
        if (fCarlaHostHandle != nullptr)
        {
            carla_host_handle_free(fCarlaHostHandle);
           #if DISTRHO_PLUGIN_NUM_INPUTS == 0 || DISTRHO_PLUGIN_NUM_OUTPUTS == 0
            delete[] fDummyBuffer;
           #endif
           #if DISTRHO_PLUGIN_WANT_MIDI_INPUT
            delete[] fMidiEvents;
           #endif
        }

        if (fCarlaPluginHandle != nullptr)
            fCarlaPluginDescriptor->cleanup(fCarlaPluginHandle);
    }

    const NativeTimeInfo* hostGetTimeInfo() const noexcept
    {
        const TimePosition& timePos(getTimePosition());

        fCarlaTimeInfo.playing = timePos.playing;
        fCarlaTimeInfo.frame = timePos.frame;
        fCarlaTimeInfo.bbt.valid = timePos.bbt.valid;
        fCarlaTimeInfo.bbt.bar = timePos.bbt.bar;
        fCarlaTimeInfo.bbt.beat = timePos.bbt.beat;
        fCarlaTimeInfo.bbt.tick = timePos.bbt.tick;
        fCarlaTimeInfo.bbt.barStartTick = timePos.bbt.barStartTick;
        fCarlaTimeInfo.bbt.beatsPerBar = timePos.bbt.beatsPerBar;
        fCarlaTimeInfo.bbt.beatType = timePos.bbt.beatType;
        fCarlaTimeInfo.bbt.ticksPerBeat = timePos.bbt.ticksPerBeat;
        fCarlaTimeInfo.bbt.beatsPerMinute = timePos.bbt.beatsPerMinute;

        return &fCarlaTimeInfo;
    }

#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
    bool hostWriteMidiEvent(const NativeMidiEvent* const event)
    {
        MidiEvent midiEvent;
        midiEvent.frame = event->time;
        midiEvent.size = event->size;
        midiEvent.dataExt = nullptr;

        uint32_t i = 0;
        for (; i < event->size; ++i)
            midiEvent.data[i] = event->data[i];
        for (; i < MidiEvent::kDataSize; ++i)
            midiEvent.data[i] = 0;

        return writeMidiEvent(midiEvent);
    }
#endif

    intptr_t hostDispatcher(const NativeHostDispatcherOpcode opcode,
                            const int32_t index, const intptr_t value, void* const ptr, const float opt)
    {
        switch (opcode)
        {
        // cannnot be supported
        case NATIVE_HOST_OPCODE_HOST_IDLE:
            break;
        // other stuff
        case NATIVE_HOST_OPCODE_NULL:
        case NATIVE_HOST_OPCODE_UPDATE_PARAMETER:
        case NATIVE_HOST_OPCODE_UPDATE_MIDI_PROGRAM:
        case NATIVE_HOST_OPCODE_RELOAD_PARAMETERS:
        case NATIVE_HOST_OPCODE_RELOAD_MIDI_PROGRAMS:
        case NATIVE_HOST_OPCODE_RELOAD_ALL:
        case NATIVE_HOST_OPCODE_UI_UNAVAILABLE:
        case NATIVE_HOST_OPCODE_INTERNAL_PLUGIN:
        case NATIVE_HOST_OPCODE_QUEUE_INLINE_DISPLAY:
        case NATIVE_HOST_OPCODE_UI_TOUCH_PARAMETER:
        case NATIVE_HOST_OPCODE_REQUEST_IDLE:
        case NATIVE_HOST_OPCODE_GET_FILE_PATH:
        case NATIVE_HOST_OPCODE_PREVIEW_BUFFER_DATA:
            // TESTING
            d_stdout("dispatcher %i:%s, %i, %li, %p, %f",
                      opcode, NativeHostDispatcherOpcode2Str(opcode), index, value, ptr, opt);
            break;
        case NATIVE_HOST_OPCODE_UI_RESIZE:
            ildaeilResizeUI(fUI, index, value);
            break;
        }

        return 0;
    }

protected:
   /* --------------------------------------------------------------------------------------------------------
    * Information */

    const char* getLabel() const override
    {
#if ILDAEIL_STANDALONE
        return "Ildaeil";
#elif DISTRHO_PLUGIN_IS_SYNTH
        return "IldaeilSynth";
#elif DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
        return "IldaeilMIDI";
#else
        return "IldaeilFX";
#endif
    }

    const char* getDescription() const override
    {
        return "Ildaeil is a mini-plugin host working as a plugin, allowing one-to-one plugin format reusage.";
    }

    const char* getMaker() const override
    {
        return "DISTRHO";
    }

    const char* getHomePage() const override
    {
        return "https://github.com/DISTRHO/Ildaeil";
    }

    const char* getLicense() const override
    {
        return "GPLv2+";
    }

    uint32_t getVersion() const override
    {
        return d_version(1, 3, 0);
    }

    int64_t getUniqueId() const override
    {
       #if ILDAEIL_STANDALONE
        return d_cconst('d', 'I', 'l', 'd');
       #elif DISTRHO_PLUGIN_IS_SYNTH
        return d_cconst('d', 'I', 'l', 'S');
       #elif DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
        return d_cconst('d', 'I', 'l', 'M');
       #else
        return d_cconst('d', 'I', 'l', 'F');
       #endif
    }

   /* --------------------------------------------------------------------------------------------------------
    * Init */

    void initAudioPort(bool input, uint32_t index, AudioPort& port) override
    {
        port.groupId = kPortGroupStereo;
        Plugin::initAudioPort(input, index, port);
    }

    void initState(const uint32_t index, State& state) override
    {
        DISTRHO_SAFE_ASSERT_RETURN(index == 0,);

        state.hints = kStateIsOnlyForDSP;
        state.key = "project";
        state.defaultValue = ""
        "<?xml version='1.0' encoding='UTF-8'?>\n"
        "<!DOCTYPE CARLA-PROJECT>\n"
        "<CARLA-PROJECT VERSION='" CARLA_VERSION_STRMIN "'>\n"
        "</CARLA-PROJECT>\n";
    }

   /* --------------------------------------------------------------------------------------------------------
    * Internal data */

    String getState(const char* const key) const override
    {
        if (std::strcmp(key, "project") == 0)
        {
            CarlaEngine* const engine = carla_get_engine_from_handle(fCarlaHostHandle);

            fLastProjectState.reset();
            engine->saveProjectInternal(fLastProjectState);
            return String(static_cast<char*>(fLastProjectState.getDataAndRelease()), false);
        }

        return String();
    }

    void setState(const char* const key, const char* const value) override
    {
        if (std::strcmp(key, "project") == 0)
        {
            CarlaEngine* const engine = carla_get_engine_from_handle(fCarlaHostHandle);

            const water::String wvalue(value);
            water::XmlDocument xml(wvalue);

            {
                const MutexLocker cml(sPluginInfoLoadMutex);
                engine->loadProjectInternal(xml, true);
            }

            if (fUI != nullptr)
                ildaeilProjectLoadedFromDSP(fUI);
        }
    }

   /* --------------------------------------------------------------------------------------------------------
    * Process */

    void checkLatencyChanged()
    {
        if (fCarlaHostHandle == nullptr)
            return;

        uint32_t latency = 0;

        for (uint32_t i=0; i < carla_get_current_plugin_count(fCarlaHostHandle); ++i)
            latency += carla_get_plugin_latency(fCarlaHostHandle, i);

        if (fLastLatencyValue != latency)
        {
            fLastLatencyValue = latency;
            setLatency(latency);
        }
    }

    void activate() override
    {
        if (fCarlaPluginHandle != nullptr)
            fCarlaPluginDescriptor->activate(fCarlaPluginHandle);

        checkLatencyChanged();
    }

    void deactivate() override
    {
        checkLatencyChanged();

        if (fCarlaPluginHandle != nullptr)
            fCarlaPluginDescriptor->deactivate(fCarlaPluginHandle);
    }

#if DISTRHO_PLUGIN_WANT_MIDI_INPUT
    void run(const float** inputs, float** outputs, uint32_t frames,
             const MidiEvent* dpfMidiEvents, uint32_t dpfMidiEventCount) override
#else
    void run(const float** inputs, float** outputs, uint32_t frames) override
#endif
    {
        if (fCarlaPluginHandle != nullptr)
        {
           #if DISTRHO_PLUGIN_WANT_MIDI_INPUT
            uint32_t midiEventCount = 0;
            for (uint32_t i=0; i < dpfMidiEventCount; ++i)
            {
                const MidiEvent& dpfMidiEvent(dpfMidiEvents[i]);
                if (dpfMidiEvent.size > 4)
                    continue;

                NativeMidiEvent& midiEvent(fMidiEvents[midiEventCount]);

                midiEvent.time = dpfMidiEvent.frame;
                midiEvent.port = 0;
                midiEvent.size = dpfMidiEvent.size;
                std::memcpy(midiEvent.data, dpfMidiEvent.data, midiEvent.size);

                if (++midiEventCount == kMaxMidiEventCount)
                    break;
            }
           #else
            static constexpr const NativeMidiEvent* fMidiEvents = nullptr;
            static constexpr const uint32_t midiEventCount = 0;
           #endif

           #if DISTRHO_PLUGIN_NUM_INPUTS == 0
            inputs = (const float**)fDummyBuffers;
           #endif
           #if DISTRHO_PLUGIN_NUM_OUTPUTS == 0
            outputs = fDummyBuffers;
           #endif

            fCarlaPluginDescriptor->process(fCarlaPluginHandle, (float**)inputs, outputs, frames,
                                            fMidiEvents, midiEventCount);

            checkLatencyChanged();
        }
        else
        {
            std::memset(outputs[0], 0, sizeof(float)*frames);
            std::memset(outputs[1], 0, sizeof(float)*frames);
        }
    }

    void bufferSizeChanged(const uint32_t newBufferSize) override
    {
       #if DISTRHO_PLUGIN_NUM_INPUTS == 0 || DISTRHO_PLUGIN_NUM_OUTPUTS == 0
        delete[] fDummyBuffer;
        fDummyBuffer = new float[newBufferSize];
        fDummyBuffers[0] = fDummyBuffer;
        fDummyBuffers[1] = fDummyBuffer;
        std::memset(fDummyBuffer, 0, sizeof(float)*newBufferSize);
       #endif

        if (fCarlaPluginHandle != nullptr)
            fCarlaPluginDescriptor->dispatcher(fCarlaPluginHandle, NATIVE_PLUGIN_OPCODE_BUFFER_SIZE_CHANGED,
                                               0, newBufferSize, nullptr, 0.0f);
    }

    void sampleRateChanged(const double newSampleRate) override
    {
        if (fCarlaPluginHandle != nullptr)
            fCarlaPluginDescriptor->dispatcher(fCarlaPluginHandle, NATIVE_PLUGIN_OPCODE_SAMPLE_RATE_CHANGED,
                                               0, 0, nullptr, newSampleRate);
    }

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

   /**
      Set our plugin class as non-copyable and add a leak detector just in case.
    */
    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(IldaeilPlugin)
};

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

static uint32_t host_get_buffer_size(const NativeHostHandle handle)
{
    return static_cast<IldaeilPlugin*>(handle)->getBufferSize();
}

static double host_get_sample_rate(const NativeHostHandle handle)
{
    return static_cast<IldaeilPlugin*>(handle)->getSampleRate();
}

static bool host_is_offline(NativeHostHandle)
{
    return false;
}

static const NativeTimeInfo* host_get_time_info(const NativeHostHandle handle)
{
    return static_cast<IldaeilPlugin*>(handle)->hostGetTimeInfo();
}

static bool host_write_midi_event(const NativeHostHandle handle, const NativeMidiEvent* const event)
{
#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
    return static_cast<IldaeilPlugin*>(handle)->hostWriteMidiEvent(event);
#else
    return handle != nullptr && event != nullptr && false;
#endif
}

static void host_ui_parameter_changed(const NativeHostHandle handle, const uint32_t index, const float value)
{
    ildaeilParameterChangeForUI(static_cast<IldaeilPlugin*>(handle)->fUI, index, value);
}

static void host_ui_midi_program_changed(NativeHostHandle handle, uint8_t channel, uint32_t bank, uint32_t program)
{
    d_stdout("%s %p %u %u %u", __FUNCTION__, handle, channel, bank, program);
}

static void host_ui_custom_data_changed(NativeHostHandle handle, const char* key, const char* value)
{
    d_stdout("%s %p %s %s", __FUNCTION__, handle, key, value);
}

static void host_ui_closed(NativeHostHandle handle)
{
    ildaeilCloseUI(static_cast<IldaeilPlugin*>(handle));
}

static const char* host_ui_open_file(const NativeHostHandle handle, const bool isDir, const char* const title, const char* const filter)
{
    return ildaeilOpenFileForUI(static_cast<IldaeilPlugin*>(handle)->fUI, isDir, title, filter);
}

static const char* host_ui_save_file(NativeHostHandle, bool, const char*, const char*)
{
    return nullptr;
}

static intptr_t host_dispatcher(const NativeHostHandle handle, const NativeHostDispatcherOpcode opcode,
                                const int32_t index, const intptr_t value, void* const ptr, const float opt)
{
    return static_cast<IldaeilPlugin*>(handle)->hostDispatcher(opcode, index, value, ptr, opt);
}

/* --------------------------------------------------------------------------------------------------------------------
 * Plugin entry point, called by DPF to create a new plugin instance. */

Plugin* createPlugin()
{
    return new IldaeilPlugin();
}

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

END_NAMESPACE_DISTRHO
