/*
 * DISTRHO Plugin Framework (DPF)
 * Copyright (C) 2012-2021 Filipe Coelho <falktx@falktx.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any purpose with
 * or without fee is hereby granted, provided that the above copyright notice and this
 * permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
 * TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN
 * NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "DistrhoUIInternal.hpp"

#include "../extra/String.hpp"

#include "lv2/atom.h"
#include "lv2/atom-util.h"
#include "lv2/data-access.h"
#include "lv2/instance-access.h"
#include "lv2/midi.h"
#include "lv2/options.h"
#include "lv2/parameters.h"
#include "lv2/patch.h"
#include "lv2/ui.h"
#include "lv2/urid.h"
#include "lv2/lv2_kxstudio_properties.h"
#include "lv2/lv2_programs.h"

#ifndef DISTRHO_PLUGIN_LV2_STATE_PREFIX
# define DISTRHO_PLUGIN_LV2_STATE_PREFIX "urn:distrho:"
#endif

START_NAMESPACE_DISTRHO

typedef struct _LV2_Atom_MidiEvent {
    LV2_Atom atom;    /**< Atom header. */
    uint8_t  data[3]; /**< MIDI data (body). */
} LV2_Atom_MidiEvent;

#if ! DISTRHO_PLUGIN_WANT_STATE
static constexpr const setStateFunc setStateCallback = nullptr;
#endif
#if ! DISTRHO_PLUGIN_WANT_MIDI_INPUT
static constexpr const sendNoteFunc sendNoteCallback = nullptr;
#endif

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

template <class LV2F>
static const LV2F* getLv2Feature(const LV2_Feature* const* features, const char* const uri)
{
    for (int i=0; features[i] != nullptr; ++i)
    {
        if (std::strcmp(features[i]->URI, uri) == 0)
            return (const LV2F*)features[i]->data;
    }

    return nullptr;
}

class UiLv2
{
public:
    UiLv2(const char* const bundlePath,
          const intptr_t winId,
          const LV2_Options_Option* options,
          const LV2_URID_Map* const uridMap,
          const LV2_Feature* const* const features,
          const LV2UI_Controller controller,
          const LV2UI_Write_Function writeFunc,
          LV2UI_Widget* const widget,
          void* const dspPtr,
          const float sampleRate,
          const float scaleFactor,
          const uint32_t bgColor,
          const uint32_t fgColor,
          const char* const appClassName)
        : fUridMap(uridMap),
          fUridUnmap(getLv2Feature<LV2_URID_Unmap>(features, LV2_URID__unmap)),
          fUiPortMap(getLv2Feature<LV2UI_Port_Map>(features, LV2_UI__portMap)),
          fUiRequestValue(getLv2Feature<LV2UI_Request_Value>(features, LV2_UI__requestValue)),
          fUiTouch(getLv2Feature<LV2UI_Touch>(features, LV2_UI__touch)),
          fController(controller),
          fWriteFunction(writeFunc),
          fURIDs(uridMap),
          fBypassParameterIndex(fUiPortMap != nullptr ? fUiPortMap->port_index(fUiPortMap->handle, "lv2_enabled")
                                                      : LV2UI_INVALID_PORT_INDEX),
          fWinIdWasNull(winId == 0),
          fUI(this, winId, sampleRate,
              editParameterCallback,
              setParameterCallback,
              setStateCallback,
              sendNoteCallback,
              nullptr, // resize is very messy, hosts can do it without extensions
              fileRequestCallback,
              bundlePath, dspPtr, scaleFactor, bgColor, fgColor, appClassName)
    {
        if (widget != nullptr)
            *widget = (LV2UI_Widget)fUI.getNativeWindowHandle();

       #if DISTRHO_PLUGIN_WANT_STATE
        // tell the DSP we're ready to receive msgs
        setState("__dpf_ui_data__", "");
       #endif

        if (winId != 0)
            return;

        // if winId == 0 then options must not be null
        DISTRHO_SAFE_ASSERT_RETURN(options != nullptr,);

       #ifndef __EMSCRIPTEN__
        const LV2_URID uridWindowTitle    = uridMap->map(uridMap->handle, LV2_UI__windowTitle);
        const LV2_URID uridTransientWinId = uridMap->map(uridMap->handle, LV2_KXSTUDIO_PROPERTIES__TransientWindowId);

        const char* windowTitle = nullptr;

        for (int i=0; options[i].key != 0; ++i)
        {
            if (options[i].key == uridTransientWinId)
            {
                if (options[i].type == fURIDs.atomLong)
                {
                    if (const int64_t transientWinId = *(const int64_t*)options[i].value)
                        fUI.setWindowTransientWinId(static_cast<intptr_t>(transientWinId));
                }
                else
                    d_stderr("Host provides transientWinId but has wrong value type");
            }
            else if (options[i].key == uridWindowTitle)
            {
                if (options[i].type == fURIDs.atomString)
                {
                    windowTitle = (const char*)options[i].value;
                }
                else
                    d_stderr("Host provides windowTitle but has wrong value type");
            }
        }

        if (windowTitle == nullptr)
            windowTitle = DISTRHO_PLUGIN_NAME;

        fUI.setWindowTitle(windowTitle);
       #endif
    }

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

    void lv2ui_port_event(const uint32_t rindex, const uint32_t bufferSize, const uint32_t format, const void* const buffer)
    {
        if (format == 0)
        {
            const uint32_t parameterOffset = fUI.getParameterOffset();

            if (rindex < parameterOffset)
                return;

            DISTRHO_SAFE_ASSERT_RETURN(bufferSize == sizeof(float),)

            float value = *(const float*)buffer;

            if (rindex == fBypassParameterIndex)
                value = 1.0f - value;

            fUI.parameterChanged(rindex-parameterOffset, value);
        }
       #if DISTRHO_PLUGIN_WANT_STATE
        else if (format == fURIDs.atomEventTransfer)
        {
            const LV2_Atom* const atom = (const LV2_Atom*)buffer;

            if (atom->type == fURIDs.dpfKeyValue)
            {
                const char* const key   = (const char*)LV2_ATOM_BODY_CONST(atom);
                const char* const value = key+(std::strlen(key)+1);

                fUI.stateChanged(key, value);
            }
            else if (atom->type == fURIDs.atomObject && fUridUnmap != nullptr)
            {
                const LV2_Atom_Object* const obj = (const LV2_Atom_Object*)atom;

                const LV2_Atom* property = nullptr;
                const LV2_Atom* atomvalue = nullptr;
                lv2_atom_object_get(obj, fURIDs.patchProperty, &property, fURIDs.patchValue, &atomvalue, 0);

                DISTRHO_SAFE_ASSERT_RETURN(property != nullptr,);
                DISTRHO_SAFE_ASSERT_RETURN(atomvalue != nullptr,);

                DISTRHO_SAFE_ASSERT_RETURN(property->type == fURIDs.atomURID,);
                DISTRHO_SAFE_ASSERT_RETURN(atomvalue->type == fURIDs.atomPath || atomvalue->type == fURIDs.atomString,);

                if (property != nullptr && property->type == fURIDs.atomURID &&
                    atomvalue != nullptr && (atomvalue->type == fURIDs.atomPath || atomvalue->type == fURIDs.atomString))
                {
                    const LV2_URID dpf_lv2_urid = ((const LV2_Atom_URID*)property)->body;
                    DISTRHO_SAFE_ASSERT_RETURN(dpf_lv2_urid != 0,);

                    const char* const dpf_lv2_key = fUridUnmap->unmap(fUridUnmap->handle, dpf_lv2_urid);
                    DISTRHO_SAFE_ASSERT_RETURN(dpf_lv2_key != nullptr,);

                    /*constexpr*/ const size_t reqLen = std::strlen(DISTRHO_PLUGIN_URI "#");
                    DISTRHO_SAFE_ASSERT_RETURN(std::strlen(dpf_lv2_key) > reqLen,);

                    const char* const key   = dpf_lv2_key + reqLen;
                    const char* const value = (const char*)LV2_ATOM_BODY_CONST(atomvalue);

                    fUI.stateChanged(key, value);
                }
            }
            else if (atom->type == fURIDs.midiEvent)
            {
                // ignore
            }
            else
            {
                d_stdout("DPF :: received atom not handled :: %s",
                         fUridUnmap != nullptr ? fUridUnmap->unmap(fUridUnmap->handle, atom->type) : "(null)");
            }
        }
       #endif
    }

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

    int lv2ui_idle()
    {
        if (fWinIdWasNull)
            return (fUI.plugin_idle() && fUI.isVisible()) ? 0 : 1;

        return fUI.plugin_idle() ? 0 : 1;
    }

    int lv2ui_show()
    {
        return fUI.setWindowVisible(true) ? 0 : 1;
    }

    int lv2ui_hide()
    {
        return fUI.setWindowVisible(false) ? 0 : 1;
    }

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

    uint32_t lv2_get_options(LV2_Options_Option* const /*options*/)
    {
        // currently unused
        return LV2_OPTIONS_ERR_UNKNOWN;
    }

    uint32_t lv2_set_options(const LV2_Options_Option* const options)
    {
        for (int i=0; options[i].key != 0; ++i)
        {
            if (options[i].key == fURIDs.paramSampleRate)
            {
                if (options[i].type == fURIDs.atomFloat)
                {
                    const float sampleRate = *(const float*)options[i].value;
                    fUI.setSampleRate(sampleRate, true);
                    continue;
                }
                else
                {
                    d_stderr("Host changed UI sample-rate but with wrong value type");
                    continue;
                }
            }
        }

        return LV2_OPTIONS_SUCCESS;
    }

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

   #if DISTRHO_PLUGIN_WANT_PROGRAMS
    void lv2ui_select_program(const uint32_t bank, const uint32_t program)
    {
        const uint32_t realProgram = bank * 128 + program;

        fUI.programLoaded(realProgram);
    }
   #endif

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

private:
    // LV2 features
    const LV2_URID_Map*        const fUridMap;
    const LV2_URID_Unmap*      const fUridUnmap;
    const LV2UI_Port_Map*      const fUiPortMap;
    const LV2UI_Request_Value* const fUiRequestValue;
    const LV2UI_Touch*         const fUiTouch;

    // LV2 UI stuff
    const LV2UI_Controller     fController;
    const LV2UI_Write_Function fWriteFunction;

    // LV2 URIDs
    const struct URIDs {
        const LV2_URID_Map* _uridMap;
        const LV2_URID dpfKeyValue;
        const LV2_URID atomEventTransfer;
        const LV2_URID atomFloat;
        const LV2_URID atomLong;
        const LV2_URID atomObject;
        const LV2_URID atomPath;
        const LV2_URID atomString;
        const LV2_URID atomURID;
        const LV2_URID midiEvent;
        const LV2_URID paramSampleRate;
        const LV2_URID patchProperty;
        const LV2_URID patchSet;
        const LV2_URID patchValue;

        URIDs(const LV2_URID_Map* const uridMap)
            : _uridMap(uridMap),
              dpfKeyValue(map(DISTRHO_PLUGIN_LV2_STATE_PREFIX "KeyValueState")),
              atomEventTransfer(map(LV2_ATOM__eventTransfer)),
              atomFloat(map(LV2_ATOM__Float)),
              atomLong(map(LV2_ATOM__Long)),
              atomObject(map(LV2_ATOM__Object)),
              atomPath(map(LV2_ATOM__Path)),
              atomString(map(LV2_ATOM__String)),
              atomURID(map(LV2_ATOM__URID)),
              midiEvent(map(LV2_MIDI__MidiEvent)),
              paramSampleRate(map(LV2_PARAMETERS__sampleRate)),
              patchProperty(map(LV2_PATCH__property)),
              patchSet(map(LV2_PATCH__Set)),
              patchValue(map(LV2_PATCH__value)) {}

        inline LV2_URID map(const char* const uri) const
        {
            return _uridMap->map(_uridMap->handle, uri);
        }
    } fURIDs;

    // index of bypass parameter, if present
    const uint32_t fBypassParameterIndex;

    // using ui:showInterface if true
    const bool fWinIdWasNull;

    // Plugin UI (after LV2 stuff so the UI can call into us during its constructor)
    UIExporter fUI;

    // ----------------------------------------------------------------------------------------------------------------
    // DPF callbacks

    void editParameterValue(const uint32_t rindex, const bool started)
    {
        if (fUiTouch != nullptr && fUiTouch->touch != nullptr)
            fUiTouch->touch(fUiTouch->handle, rindex, started);
    }

    static void editParameterCallback(void* const ptr, const uint32_t rindex, const bool started)
    {
        static_cast<UiLv2*>(ptr)->editParameterValue(rindex, started);
    }

    void setParameterValue(const uint32_t rindex, float value)
    {
        DISTRHO_SAFE_ASSERT_RETURN(fWriteFunction != nullptr,);

        if (rindex == fBypassParameterIndex)
            value = 1.0f - value;

        fWriteFunction(fController, rindex, sizeof(float), 0, &value);
    }

    static void setParameterCallback(void* const ptr, const uint32_t rindex, const float value)
    {
        static_cast<UiLv2*>(ptr)->setParameterValue(rindex, value);
    }

   #if DISTRHO_PLUGIN_WANT_STATE
    void setState(const char* const key, const char* const value)
    {
        DISTRHO_SAFE_ASSERT_RETURN(fWriteFunction != nullptr,);

        const uint32_t eventInPortIndex = DISTRHO_PLUGIN_NUM_INPUTS + DISTRHO_PLUGIN_NUM_OUTPUTS;

        // join key and value
        String tmpStr;
        tmpStr += key;
        tmpStr += "\xff";
        tmpStr += value;

        tmpStr[std::strlen(key)] = '\0';

        // set msg size (key + separator + value + null terminator)
        const uint32_t msgSize = static_cast<uint32_t>(tmpStr.length()) + 1U;

        // reserve atom space
        const uint32_t atomSize = sizeof(LV2_Atom) + msgSize;
        char* const  atomBuf = (char*)malloc(atomSize);
        DISTRHO_SAFE_ASSERT_RETURN(atomBuf != nullptr,);

        std::memset(atomBuf, 0, atomSize);

        // set atom info
        LV2_Atom* const atom = (LV2_Atom*)atomBuf;
        atom->size = msgSize;
        atom->type = fURIDs.dpfKeyValue;

        // set atom data
        std::memcpy(atomBuf + sizeof(LV2_Atom), tmpStr.buffer(), msgSize);

        // send to DSP side
        fWriteFunction(fController, eventInPortIndex, atomSize, fURIDs.atomEventTransfer, atom);

        // free atom space
        free(atomBuf);
    }

    static void setStateCallback(void* const ptr, const char* const key, const char* const value)
    {
        static_cast<UiLv2*>(ptr)->setState(key, value);
    }
   #endif

   #if DISTRHO_PLUGIN_WANT_MIDI_INPUT
    void sendNote(const uint8_t channel, const uint8_t note, const uint8_t velocity)
    {
        DISTRHO_SAFE_ASSERT_RETURN(fWriteFunction != nullptr,);

        if (channel > 0xF)
            return;

        const uint32_t eventInPortIndex = DISTRHO_PLUGIN_NUM_INPUTS + DISTRHO_PLUGIN_NUM_OUTPUTS;

        LV2_Atom_MidiEvent atomMidiEvent;
        atomMidiEvent.atom.size = 3;
        atomMidiEvent.atom.type = fURIDs.midiEvent;

        atomMidiEvent.data[0] = channel + (velocity != 0 ? 0x90 : 0x80);
        atomMidiEvent.data[1] = note;
        atomMidiEvent.data[2] = velocity;

        // send to DSP side
        fWriteFunction(fController, eventInPortIndex, lv2_atom_total_size(&atomMidiEvent.atom),
                       fURIDs.atomEventTransfer, &atomMidiEvent);
    }

    static void sendNoteCallback(void* const ptr, const uint8_t channel, const uint8_t note, const uint8_t velocity)
    {
        static_cast<UiLv2*>(ptr)->sendNote(channel, note, velocity);
    }
   #endif

    bool fileRequest(const char* const key)
    {
        d_stdout("UI file request %s %p", key, fUiRequestValue);

        if (fUiRequestValue == nullptr)
            return false;

        String dpf_lv2_key(DISTRHO_PLUGIN_URI "#");
        dpf_lv2_key += key;

        const int r = fUiRequestValue->request(fUiRequestValue->handle,
                                        fUridMap->map(fUridMap->handle, dpf_lv2_key.buffer()),
                                        fURIDs.atomPath,
                                        nullptr);

        d_stdout("UI file request %s %p => %s %i", key, fUiRequestValue, dpf_lv2_key.buffer(), r);
        return r == LV2UI_REQUEST_VALUE_SUCCESS;
    }

    static bool fileRequestCallback(void* ptr, const char* key)
    {
        return static_cast<UiLv2*>(ptr)->fileRequest(key);
    }
};

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

static LV2UI_Handle lv2ui_instantiate(const LV2UI_Descriptor*,
                                      const char* const uri,
                                      const char* const bundlePath,
                                      const LV2UI_Write_Function writeFunction,
                                      const LV2UI_Controller controller,
                                      LV2UI_Widget* const widget,
                                      const LV2_Feature* const* const features)
{
    if (uri == nullptr || std::strcmp(uri, DISTRHO_PLUGIN_URI) != 0)
    {
        d_stderr("Invalid plugin URI");
        return nullptr;
    }

    const LV2_Options_Option* options   = nullptr;
    const LV2_URID_Map*       uridMap   = nullptr;
    void*                     parentId  = nullptr;
    void*                     instance  = nullptr;

#if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
    struct LV2_DirectAccess_Interface {
        void* (*get_instance_pointer)(LV2_Handle handle);
    };
    const LV2_Extension_Data_Feature* extData = nullptr;
#endif

    for (int i=0; features[i] != nullptr; ++i)
    {
        /**/ if (std::strcmp(features[i]->URI, LV2_OPTIONS__options) == 0)
            options = (const LV2_Options_Option*)features[i]->data;
        else if (std::strcmp(features[i]->URI, LV2_URID__map) == 0)
            uridMap = (const LV2_URID_Map*)features[i]->data;
        else if (std::strcmp(features[i]->URI, LV2_UI__parent) == 0)
            parentId = features[i]->data;
#if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
        else if (std::strcmp(features[i]->URI, LV2_DATA_ACCESS_URI) == 0)
            extData = (const LV2_Extension_Data_Feature*)features[i]->data;
        else if (std::strcmp(features[i]->URI, LV2_INSTANCE_ACCESS_URI) == 0)
            instance = features[i]->data;
#endif
    }

    if (options == nullptr && parentId == nullptr)
    {
        d_stderr("Options feature missing (needed for show-interface), cannot continue!");
        return nullptr;
    }

    if (uridMap == nullptr)
    {
        d_stderr("URID Map feature missing, cannot continue!");
        return nullptr;
    }

    if (parentId == nullptr)
    {
        d_stdout("Parent Window Id missing, host should be using ui:showInterface...");
    }

#if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
    if (extData == nullptr || instance == nullptr)
    {
        d_stderr("Data or instance access missing, cannot continue!");
        return nullptr;
    }

    if (const LV2_DirectAccess_Interface* const directAccess = (const LV2_DirectAccess_Interface*)extData->data_access(DISTRHO_PLUGIN_LV2_STATE_PREFIX "direct-access"))
        instance = directAccess->get_instance_pointer(instance);
    else
        instance = nullptr;

    if (instance == nullptr)
    {
        d_stderr("Failed to get direct access, cannot continue!");
        return nullptr;
    }
#endif

    const intptr_t winId = (intptr_t)parentId;
    float sampleRate = 0.0f;
    float scaleFactor = 0.0f;
    uint32_t bgColor = 0;
    uint32_t fgColor = 0xffffffff;
    const char* appClassName = nullptr;

    if (options != nullptr)
    {
        const LV2_URID uridAtomInt     = uridMap->map(uridMap->handle, LV2_ATOM__Int);
        const LV2_URID uridAtomFloat   = uridMap->map(uridMap->handle, LV2_ATOM__Float);
        const LV2_URID uridAtomString  = uridMap->map(uridMap->handle, LV2_ATOM__String);
        const LV2_URID uridSampleRate  = uridMap->map(uridMap->handle, LV2_PARAMETERS__sampleRate);
        const LV2_URID uridBgColor     = uridMap->map(uridMap->handle, LV2_UI__backgroundColor);
        const LV2_URID uridFgColor     = uridMap->map(uridMap->handle, LV2_UI__foregroundColor);
       #ifndef DISTRHO_OS_MAC
        const LV2_URID uridScaleFactor = uridMap->map(uridMap->handle, LV2_UI__scaleFactor);
       #endif
        const LV2_URID uridClassName   = uridMap->map(uridMap->handle, "urn:distrho:className");

        for (int i=0; options[i].key != 0; ++i)
        {
            /**/ if (options[i].key == uridSampleRate)
            {
                if (options[i].type == uridAtomFloat)
                    sampleRate = *(const float*)options[i].value;
                else
                    d_stderr("Host provides UI sample-rate but has wrong value type");
            }
            else if (options[i].key == uridBgColor)
            {
                if (options[i].type == uridAtomInt)
                    bgColor = (uint32_t)*(const int32_t*)options[i].value;
                else
                    d_stderr("Host provides UI background color but has wrong value type");
            }
            else if (options[i].key == uridFgColor)
            {
                if (options[i].type == uridAtomInt)
                    fgColor = (uint32_t)*(const int32_t*)options[i].value;
                else
                    d_stderr("Host provides UI foreground color but has wrong value type");
            }
           #ifndef DISTRHO_OS_MAC
            else if (options[i].key == uridScaleFactor)
            {
                if (options[i].type == uridAtomFloat)
                    scaleFactor = *(const float*)options[i].value;
                else
                    d_stderr("Host provides UI scale factor but has wrong value type");
            }
           #endif
            else if (options[i].key == uridClassName)
            {
                if (options[i].type == uridAtomString)
                    appClassName = (const char*)options[i].value;
                else
                    d_stderr("Host provides UI scale factor but has wrong value type");
            }
        }
    }

    if (sampleRate < 1.0)
    {
        d_stdout("WARNING: this host does not send sample-rate information for LV2 UIs, using 44100 as fallback (this could be wrong)");
        sampleRate = 44100.0;
    }

    return new UiLv2(bundlePath, winId, options, uridMap, features,
                     controller, writeFunction, widget, instance,
                     sampleRate, scaleFactor, bgColor, fgColor, appClassName);
}

#define uiPtr ((UiLv2*)ui)

static void lv2ui_cleanup(LV2UI_Handle ui)
{
    delete uiPtr;
}

static void lv2ui_port_event(LV2UI_Handle ui, uint32_t portIndex, uint32_t bufferSize, uint32_t format, const void* buffer)
{
    uiPtr->lv2ui_port_event(portIndex, bufferSize, format, buffer);
}

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

static int lv2ui_idle(LV2UI_Handle ui)
{
    return uiPtr->lv2ui_idle();
}

static int lv2ui_show(LV2UI_Handle ui)
{
    return uiPtr->lv2ui_show();
}

static int lv2ui_hide(LV2UI_Handle ui)
{
    return uiPtr->lv2ui_hide();
}

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

static uint32_t lv2_get_options(LV2UI_Handle ui, LV2_Options_Option* options)
{
    return uiPtr->lv2_get_options(options);
}

static uint32_t lv2_set_options(LV2UI_Handle ui, const LV2_Options_Option* options)
{
    return uiPtr->lv2_set_options(options);
}

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

#if DISTRHO_PLUGIN_WANT_PROGRAMS
static void lv2ui_select_program(LV2UI_Handle ui, uint32_t bank, uint32_t program)
{
    uiPtr->lv2ui_select_program(bank, program);
}
#endif

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

static const void* lv2ui_extension_data(const char* uri)
{
    static const LV2_Options_Interface options = { lv2_get_options, lv2_set_options };
    static const LV2UI_Idle_Interface  uiIdle  = { lv2ui_idle };
    static const LV2UI_Show_Interface  uiShow  = { lv2ui_show, lv2ui_hide };

    if (std::strcmp(uri, LV2_OPTIONS__interface) == 0)
        return &options;
    if (std::strcmp(uri, LV2_UI__idleInterface) == 0)
        return &uiIdle;
    if (std::strcmp(uri, LV2_UI__showInterface) == 0)
        return &uiShow;

#if DISTRHO_PLUGIN_WANT_PROGRAMS
    static const LV2_Programs_UI_Interface uiPrograms = { lv2ui_select_program };

    if (std::strcmp(uri, LV2_PROGRAMS__UIInterface) == 0)
        return &uiPrograms;
#endif

    return nullptr;
}

#undef instancePtr

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

static const LV2UI_Descriptor sLv2UiDescriptor = {
    DISTRHO_UI_URI,
    lv2ui_instantiate,
    lv2ui_cleanup,
    lv2ui_port_event,
    lv2ui_extension_data
};

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

END_NAMESPACE_DISTRHO

DISTRHO_PLUGIN_EXPORT
const LV2UI_Descriptor* lv2ui_descriptor(uint32_t index)
{
    USE_NAMESPACE_DISTRHO
    return (index == 0) ? &sLv2UiDescriptor : nullptr;
}

#if defined(__MOD_DEVICES__) && defined(__EMSCRIPTEN__)
#include <emscripten/html5.h>
#include <string>

typedef void (*_custom_param_set)(uint32_t port_index, float value);
typedef void (*_custom_patch_set)(const char* uri, const char* value);

struct ModguiHandle {
    LV2UI_Handle handle;
    long loop_id;
    _custom_param_set param_set;
    _custom_patch_set patch_set;
};

enum URIs {
    kUriNull,
    kUriAtomEventTransfer,
    kUriDpfKeyValue,
};

static std::vector<std::string> kURIs;

static LV2_URID lv2_urid_map(LV2_URID_Map_Handle, const char* const uri)
{
    for (size_t i=0, size=kURIs.size(); i<size; ++i)
    {
        if (kURIs[i] == uri)
            return i;
    }

    kURIs.push_back(uri);
    return kURIs.size() - 1u;
}

static const char* lv2_urid_unmap(LV2_URID_Map_Handle, const LV2_URID urid)
{
    return kURIs[urid].c_str();
}

static void lv2ui_write_function(LV2UI_Controller controller,
                                 uint32_t         port_index,
                                 uint32_t         buffer_size,
                                 uint32_t         port_protocol,
                                 const void*      buffer)
{
    DISTRHO_SAFE_ASSERT_RETURN(buffer_size >= 1,);

    // d_stdout("lv2ui_write_function %p %u %u %u %p", controller, port_index, buffer_size, port_protocol, buffer);
    ModguiHandle* const mhandle = static_cast<ModguiHandle*>(controller);

    switch (port_protocol)
    {
    case kUriNull:
        mhandle->param_set(port_index, *static_cast<const float*>(buffer));
        break;
    case kUriAtomEventTransfer:
        if (const LV2_Atom* const atom = static_cast<const LV2_Atom*>(buffer))
        {
            // d_stdout("lv2ui_write_function %u %u:%s", atom->size, atom->type, kURIs[atom->type].c_str());

            // if (kURIs[atom->type] == "urn:distrho:KeyValueState")
            {
                const char* const key   = (const char*)(atom + 1);
                const char* const value = key + (std::strlen(key) + 1U);
                // d_stdout("lv2ui_write_function %s %s", key, value);

                String urikey;
                urikey  = DISTRHO_PLUGIN_URI "#";
                urikey += key;

                mhandle->patch_set(urikey, value);
            }
        }
        break;
    }
}

static void app_idle(void* const handle)
{
    static_cast<UiLv2*>(handle)->lv2ui_idle();
}

DISTRHO_PLUGIN_EXPORT
LV2UI_Handle modgui_init(const char* const className, _custom_param_set param_set, _custom_patch_set patch_set)
{
    d_stdout("init \"%s\"", className);
    DISTRHO_SAFE_ASSERT_RETURN(className != nullptr, nullptr);

    static LV2_URID_Map uridMap = { nullptr, lv2_urid_map };
    static LV2_URID_Unmap uridUnmap = { nullptr, lv2_urid_unmap };

    // known first URIDs, matching URIs
    if (kURIs.empty())
    {
        kURIs.push_back("");
        kURIs.push_back("http://lv2plug.in/ns/ext/atom#eventTransfer");
        kURIs.push_back(DISTRHO_PLUGIN_LV2_STATE_PREFIX "KeyValueState");
    }

    static float sampleRateValue = 48000.f;
    static LV2_Options_Option options[3] = {
        {
            LV2_OPTIONS_INSTANCE,
            0,
            uridMap.map(uridMap.handle, LV2_PARAMETERS__sampleRate),
            sizeof(float),
            uridMap.map(uridMap.handle, LV2_ATOM__Float),
            &sampleRateValue
        },
        {
            LV2_OPTIONS_INSTANCE,
            0,
            uridMap.map(uridMap.handle, "urn:distrho:className"),
            std::strlen(className) + 1,
            uridMap.map(uridMap.handle, LV2_ATOM__String),
            className
        },
        {}
    };

    static const LV2_Feature optionsFt = { LV2_OPTIONS__options, static_cast<void*>(options) };
    static const LV2_Feature uridMapFt = { LV2_URID__map, static_cast<void*>(&uridMap) };
    static const LV2_Feature uridUnmapFt = { LV2_URID__unmap, static_cast<void*>(&uridUnmap) };

    static const LV2_Feature* features[] = {
        &optionsFt,
        &uridMapFt,
        &uridUnmapFt,
        nullptr
    };

    ModguiHandle* const mhandle = new ModguiHandle;
    mhandle->handle = nullptr;
    mhandle->loop_id = 0;
    mhandle->param_set = param_set;
    mhandle->patch_set = patch_set;

    LV2UI_Widget widget;
    const LV2UI_Handle handle = lv2ui_instantiate(&sLv2UiDescriptor,
                                                  DISTRHO_PLUGIN_URI,
                                                  "", // bundlePath
                                                  lv2ui_write_function,
                                                  mhandle,
                                                  &widget,
                                                  features);
    mhandle->handle = handle;

    static_cast<UiLv2*>(handle)->lv2ui_show();
    mhandle->loop_id = emscripten_set_interval(app_idle, 1000.0/60, handle);

    return mhandle;
}

DISTRHO_PLUGIN_EXPORT
void modgui_param_set(const LV2UI_Handle handle, const uint32_t index, const float value)
{
    lv2ui_port_event(static_cast<ModguiHandle*>(handle)->handle, index, sizeof(float), kUriNull, &value);
}

DISTRHO_PLUGIN_EXPORT
void modgui_patch_set(const LV2UI_Handle handle, const char* const uri, const char* const value)
{
    static const constexpr uint32_t URI_PREFIX_LEN = sizeof(DISTRHO_PLUGIN_URI);
    DISTRHO_SAFE_ASSERT_RETURN(std::strncmp(uri, DISTRHO_PLUGIN_URI "#", URI_PREFIX_LEN) == 0,);

    const uint32_t keySize = std::strlen(uri + URI_PREFIX_LEN) + 1;
    const uint32_t valueSize = std::strlen(value) + 1;
    const uint32_t atomSize = sizeof(LV2_Atom) + keySize + valueSize;

    LV2_Atom* const atom = static_cast<LV2_Atom*>(std::malloc(atomSize));
    atom->size = atomSize;
    atom->type = kUriDpfKeyValue;

    std::memcpy(static_cast<uint8_t*>(static_cast<void*>(atom + 1)), uri + URI_PREFIX_LEN, keySize);
    std::memcpy(static_cast<uint8_t*>(static_cast<void*>(atom + 1)) + keySize, value, valueSize);

    lv2ui_port_event(static_cast<ModguiHandle*>(handle)->handle,
                     DISTRHO_PLUGIN_NUM_INPUTS + DISTRHO_PLUGIN_NUM_OUTPUTS, // events input port
                     atomSize, kUriAtomEventTransfer, atom);

    std::free(atom);
}

DISTRHO_PLUGIN_EXPORT
void modgui_cleanup(const LV2UI_Handle handle)
{
    d_stdout("cleanup");
    ModguiHandle* const mhandle = static_cast<ModguiHandle*>(handle);
    if (mhandle->loop_id != 0)
        emscripten_clear_interval(mhandle->loop_id);
    lv2ui_cleanup(mhandle->handle);
    delete mhandle;
}
#endif

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