/*
 * DISTRHO Cardinal Plugin
 * Copyright (C) 2021-2024 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 3 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 <app/MenuBar.hpp>
#include <app/Scene.hpp>
#include <asset.hpp>
#include <context.hpp>
#include <engine/Engine.hpp>
#include <helpers.hpp>
#include <patch.hpp>
#include <settings.hpp>
#include <string.hpp>
#include <system.hpp>
#include <ui/Button.hpp>
#include <ui/MenuItem.hpp>
#include <ui/MenuSeparator.hpp>
#include <window/Window.hpp>

#ifdef DPF_RUNTIME_TESTING
# include <plugin.hpp>
#endif

#ifdef DISTRHO_OS_WASM
# include <ui/Button.hpp>
# include <ui/Label.hpp>
# include <ui/MenuOverlay.hpp>
# include <ui/SequentialLayout.hpp>
# include <emscripten/emscripten.h>
#endif

#ifdef NDEBUG
# undef DEBUG
#endif

#include "Application.hpp"
#include "AsyncDialog.hpp"
#include "CardinalCommon.hpp"
#include "CardinalPluginContext.hpp"
#include "WindowParameters.hpp"
#include "extra/Base64.hpp"

#ifndef DISTRHO_OS_WASM
# include "extra/SharedResourcePointer.hpp"
#endif

#ifndef HEADLESS
# include "extra/ScopedValueSetter.hpp"
#endif

namespace rack {
#ifdef DISTRHO_OS_WASM
namespace asset {
std::string patchesPath();
}
#endif
namespace engine {
void Engine_setAboutToClose(Engine*);
void Engine_setRemoteDetails(Engine*, remoteUtils::RemoteDetails*);
}
namespace window {
    void WindowSetPluginUI(Window* window, CardinalBaseUI* ui);
    void WindowSetMods(Window* window, int mods);
    void WindowSetInternalSize(rack::window::Window* window, math::Vec size);
}
}

START_NAMESPACE_DISTRHO

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

#if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
uint32_t Plugin::getBufferSize() const noexcept { return 0; }
double Plugin::getSampleRate() const noexcept { return 0.0; }
const char* Plugin::getBundlePath() const noexcept { return nullptr; }
bool Plugin::isSelfTestInstance() const noexcept { return false; }
bool Plugin::writeMidiEvent(const MidiEvent&) noexcept { return false; }
#endif

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

#if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI
struct WasmWelcomeDialog : rack::widget::OpaqueWidget
{
    static const constexpr float margin = 10;
    static const constexpr float buttonWidth = 110;

    WasmWelcomeDialog()
    {
        using rack::ui::Button;
        using rack::ui::Label;
        using rack::ui::MenuOverlay;
        using rack::ui::SequentialLayout;

        box.size = rack::math::Vec(550, 310);

        SequentialLayout* const layout = new SequentialLayout;
        layout->box.pos = rack::math::Vec(0, 0);
        layout->box.size = box.size;
        layout->orientation = SequentialLayout::VERTICAL_ORIENTATION;
        layout->margin = rack::math::Vec(margin, margin);
        layout->spacing = rack::math::Vec(margin, margin);
        layout->wrap = false;
        addChild(layout);

        SequentialLayout* const contentLayout = new SequentialLayout;
        contentLayout->spacing = rack::math::Vec(margin, margin);
        layout->addChild(contentLayout);

        SequentialLayout* const buttonLayout = new SequentialLayout;
        buttonLayout->alignment = SequentialLayout::CENTER_ALIGNMENT;
        buttonLayout->box.size = box.size;
        buttonLayout->spacing = rack::math::Vec(margin, margin);
        layout->addChild(buttonLayout);

        Label* const label = new Label;
        label->box.size.x = box.size.x - 2*margin;
        label->box.size.y = box.size.y - 2*margin - 40;
        label->fontSize = 20;
        label->text = ""
            "Welcome to Cardinal on the Web!\n"
            "\n"
            "If using mobile/touch devices, please note:\n"
            " - Single quick press does simple mouse click\n"
            " - Press & move does click & drag action\n"
            " - Press & hold does right-click (and opens module browser)\n"
            "\n"
            "Still a bit experimental, so proceed with caution.\n"
            "Have fun!";
        contentLayout->addChild(label);

        struct JoinDiscussionButton : Button {
            WasmWelcomeDialog* dialog;
            void onAction(const ActionEvent& e) override {
                patchUtils::openBrowser("https://github.com/DISTRHO/Cardinal/issues/287");
                dialog->getParent()->requestDelete();
            }
        };
        JoinDiscussionButton* const discussionButton = new JoinDiscussionButton;
        discussionButton->box.size.x = buttonWidth;
        discussionButton->text = "Join discussion";
        discussionButton->dialog = this;
        buttonLayout->addChild(discussionButton);

        struct DismissButton : Button {
            WasmWelcomeDialog* dialog;
            void onAction(const ActionEvent& e) override {
                dialog->getParent()->requestDelete();
            }
        };
        DismissButton* const dismissButton = new DismissButton;
        dismissButton->box.size.x = buttonWidth;
        dismissButton->text = "Dismiss";
        dismissButton->dialog = this;
        buttonLayout->addChild(dismissButton);

        MenuOverlay* const overlay = new MenuOverlay;
        overlay->bgColor = nvgRGBAf(0, 0, 0, 0.33);
        overlay->addChild(this);
        APP->scene->addChild(overlay);
    }

    void step() override
    {
        OpaqueWidget::step();
        box.pos = parent->box.size.minus(box.size).div(2).round();
    }

    void draw(const DrawArgs& args) override
    {
        bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, 0);
        Widget::draw(args);
    }
};

struct WasmRemotePatchLoadingDialog : rack::widget::OpaqueWidget
{
    static const constexpr float margin = 10;

    rack::ui::MenuOverlay* overlay;

    WasmRemotePatchLoadingDialog(const bool isFromPatchStorage)
    {
        using rack::ui::Label;
        using rack::ui::MenuOverlay;
        using rack::ui::SequentialLayout;

        box.size = rack::math::Vec(300, 40);

        SequentialLayout* const layout = new SequentialLayout;
        layout->box.pos = rack::math::Vec(0, 0);
        layout->box.size = box.size;
        layout->alignment = SequentialLayout::CENTER_ALIGNMENT;
        layout->margin = rack::math::Vec(margin, margin);
        layout->spacing = rack::math::Vec(margin, margin);
        layout->wrap = false;
        addChild(layout);

        Label* const label = new Label;
        label->box.size.x = box.size.x - 2*margin;
        label->box.size.y = box.size.y - 2*margin;
        label->fontSize = 16;
        label->text = isFromPatchStorage
                    ? "Loading patch from PatchStorage...\n"
                    : "Loading remote patch...\n";
        layout->addChild(label);

        overlay = new MenuOverlay;
        overlay->bgColor = nvgRGBAf(0, 0, 0, 0.33);
        overlay->addChild(this);
        APP->scene->addChild(overlay);
    }

    void step() override
    {
        OpaqueWidget::step();
        box.pos = parent->box.size.minus(box.size).div(2).round();
    }

    void draw(const DrawArgs& args) override
    {
        bndMenuBackground(args.vg, 0.0, 0.0, box.size.x, box.size.y, 0);
        Widget::draw(args);
    }
};

static void downloadRemotePatchFailed(const char* const filename)
{
    d_stdout("downloadRemotePatchFailed %s", filename);
    CardinalPluginContext* const context = static_cast<CardinalPluginContext*>(APP);
    CardinalBaseUI* const ui = static_cast<CardinalBaseUI*>(context->ui);

    if (ui->psDialog != nullptr)
    {
        ui->psDialog->overlay->requestDelete();
        ui->psDialog = nullptr;
        asyncDialog::create("Failed to fetch remote patch");
    }

    using namespace rack;
    context->patch->templatePath = rack::system::join(asset::patchesPath(), "templates/main.vcv");
    context->patch->loadTemplate();
    context->scene->rackScroll->reset();
}

static void downloadRemotePatchSucceeded(const char* const filename)
{
    d_stdout("downloadRemotePatchSucceeded %s | %s", filename, APP->patch->templatePath.c_str());
    CardinalPluginContext* const context = static_cast<CardinalPluginContext*>(APP);
    CardinalBaseUI* const ui = static_cast<CardinalBaseUI*>(context->ui);

    ui->psDialog->overlay->requestDelete();
    ui->psDialog = nullptr;

    if (FILE* f = fopen(filename, "r"))
    {
        uint8_t buf[8] = {};
        fread(buf, 8, 1, f);
        d_stdout("read patch %x %x %x %x %x %x %x %x",
                buf[0],buf[1],buf[2],buf[3],buf[4],buf[5],buf[6],buf[7]);
        fclose(f);
    }

    try {
        context->patch->load(filename);
    } catch (rack::Exception& e) {
        const std::string message = rack::string::f("Could not load patch: %s", e.what());
        asyncDialog::create(message.c_str());
        return;
    }

    context->patch->path.clear();
    context->scene->rackScroll->reset();
    context->history->setSaved();
}
#endif

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

class CardinalUI : public CardinalBaseUI,
                   public WindowParametersCallback
{
  #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
   #ifdef DISTRHO_OS_WASM
    ScopedPointer<Initializer> fInitializer;
   #else
    SharedResourcePointer<Initializer> fInitializer;
   #endif
    std::string fAutosavePath;
  #endif

    rack::math::Vec lastMousePos;
    WindowParameters windowParameters;
    int rateLimitStep = 0;
   #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI
    int8_t counterForFirstIdlePoint = 0;
   #endif
   #ifdef DPF_RUNTIME_TESTING
    bool inSelfTest = false;
   #endif

    struct ScopedContext {
        CardinalPluginContext* const context;

        ScopedContext(CardinalUI* const ui)
            : context(ui->context)
        {
            rack::contextSet(context);
            WindowParametersRestore(context->window);
        }

        ScopedContext(CardinalUI* const ui, const int mods)
            : context(ui->context)
        {
            rack::contextSet(context);
            rack::window::WindowSetMods(context->window, mods);
            WindowParametersRestore(context->window);
        }

        ~ScopedContext()
        {
            if (context->window != nullptr)
                WindowParametersSave(context->window);
        }
    };

public:
    CardinalUI()
        : CardinalBaseUI(DISTRHO_UI_DEFAULT_WIDTH, DISTRHO_UI_DEFAULT_HEIGHT),
        #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
         #ifdef DISTRHO_OS_WASM
          fInitializer(new Initializer(static_cast<const CardinalBasePlugin*>(nullptr), this)),
         #else
          fInitializer(static_cast<const CardinalBasePlugin*>(nullptr), this),
         #endif
        #endif
          lastMousePos()
    {
        rack::contextSet(context);

       #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
        // create unique temporary path for this instance
        try {
            char uidBuf[24];
            const std::string tmp = rack::system::getTempDirectory();

            for (int i=1;; ++i)
            {
                std::snprintf(uidBuf, sizeof(uidBuf), "Cardinal.%04d", i);
                const std::string trypath = rack::system::join(tmp, uidBuf);

                if (! rack::system::exists(trypath))
                {
                    if (rack::system::createDirectories(trypath))
                        fAutosavePath = trypath;
                    break;
                }
            }
        } DISTRHO_SAFE_EXCEPTION("create unique temporary path");

        const float sampleRate = 60; // fake audio running at 60 fps
        rack::settings::sampleRate = sampleRate;

        context->dataIns = new const float*[DISTRHO_PLUGIN_NUM_INPUTS];
        context->dataOuts = new float*[DISTRHO_PLUGIN_NUM_OUTPUTS];

        for (uint32_t i=0; i<DISTRHO_PLUGIN_NUM_INPUTS;++i)
        {
            float** const bufferptr = const_cast<float**>(&context->dataIns[i]);
            *bufferptr = new float[1];
            (*bufferptr)[0] = 0.f;
        }
        for (uint32_t i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS;++i)
            context->dataOuts[i] = new float[1];

        context->bufferSize = 1;
        context->sampleRate = sampleRate;

        context->engine = new rack::engine::Engine;
        context->engine->setSampleRate(sampleRate);

        context->history = new rack::history::State;
        context->patch = new rack::patch::Manager;
        context->patch->autosavePath = fAutosavePath;
        context->patch->templatePath = context->patch->factoryTemplatePath = fInitializer->factoryTemplatePath;

        context->event = new rack::widget::EventState;
        context->scene = new rack::app::Scene;
        context->event->rootWidget = context->scene;

        context->window = new rack::window::Window;

        context->patch->loadTemplate();
        context->scene->rackScroll->reset();

        DISTRHO_SAFE_ASSERT(remoteUtils::connectToRemote(CARDINAL_DEFAULT_REMOTE_URL));

        Engine_setRemoteDetails(context->engine, remoteDetails);
       #endif

        Window& window(getWindow());

        window.setIgnoringKeyRepeat(true);
        context->nativeWindowId = window.getNativeWindowHandle();

        const double scaleFactor = getScaleFactor();

        setGeometryConstraints(648 * scaleFactor, 538 * scaleFactor);

        if (rack::isStandalone() && rack::system::exists(rack::settings::settingsPath))
        {
            const double width = std::max(648.f, rack::settings::windowSize.x) * scaleFactor;
            const double height = std::max(538.f, rack::settings::windowSize.y) * scaleFactor;
            setSize(width, height);
        }

       #if DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
        const DGL_NAMESPACE::Window::ScopedGraphicsContext sgc(window);
       #endif

        rack::window::WindowSetPluginUI(context->window, this);

        // hide "Browse VCV Library" button
        rack::widget::Widget* const browser = context->scene->browser->children.back();
        rack::widget::Widget* const headerLayout = browser->children.front();
        rack::widget::Widget* const libraryButton = headerLayout->children.back();
        libraryButton->hide();

        // Report to user if something is wrong with the installation
        std::string errorMessage;

        if (rack::asset::systemDir.empty())
        {
            errorMessage = "Failed to locate Cardinal plugin bundle.\n"
                           "Install Cardinal with its plugin bundle folder intact and try again.";
        }
        else if (! rack::system::exists(rack::asset::systemDir))
        {
            errorMessage = rack::string::f("System directory \"%s\" does not exist. "
                                           "Make sure Cardinal was downloaded and installed correctly.",
                                           rack::asset::systemDir.c_str());
        }

        if (! errorMessage.empty())
        {
            static bool shown = false;

            if (! shown)
            {
                shown = true;
                asyncDialog::create(errorMessage.c_str());
            }
        }

       #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI
        if (rack::patchStorageSlug != nullptr)
        {
            psDialog = new WasmRemotePatchLoadingDialog(true);
        }
        else if (rack::patchRemoteURL != nullptr)
        {
            psDialog = new WasmRemotePatchLoadingDialog(false);
        }
        else if (rack::patchFromURL != nullptr)
        {
            static_cast<CardinalBasePlugin*>(context->plugin)->setState("patch", rack::patchFromURL);
            rack::contextSet(context);
        }
        else
        {
            new WasmWelcomeDialog();
        }
       #endif

        context->window->step();

        rack::contextSet(nullptr);

        WindowParametersSetCallback(context->window, this);
    }

    ~CardinalUI() override
    {
        rack::contextSet(context);

        context->nativeWindowId = 0;

        rack::window::WindowSetPluginUI(context->window, nullptr);

        context->tlw = nullptr;
        context->ui = nullptr;

       #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
        {
            const ScopedContext sc(this);
            context->patch->clear();

            // do a little dance to prevent context scene deletion from saving to temp dir
            const ScopedValueSetter<bool> svs(rack::settings::headless, true);
            Engine_setAboutToClose(context->engine);
            delete context;
        }

        if (! fAutosavePath.empty())
            rack::system::removeRecursively(fAutosavePath);
       #endif

       #if ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
        if (fInitializer->shouldSaveSettings)
        {
            INFO("Save settings");
            rack::settings::save();
        }
       #endif

        rack::contextSet(nullptr);
    }

    void onNanoDisplay() override
    {
        const ScopedContext sc(this);
        context->window->step();
    }

    void uiIdle() override
    {
       #ifdef DPF_RUNTIME_TESTING
        if (inSelfTest)
        {
            context->window->step();
            return;
        }

        if (context->plugin->isSelfTestInstance())
        {
            inSelfTest = true;

            Application& app(getApp());

            const ScopedContext sc(this);

            context->patch->clear();
            app.idle();

            const rack::math::Vec mousePos(getWidth()/2,getHeight()/2);
            context->event->handleButton(mousePos, GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE, 0x0);
            context->event->handleHover(mousePos, rack::math::Vec(0,0));
            app.idle();

            for (rack::plugin::Plugin* p : rack::plugin::plugins)
            {
                for (rack::plugin::Model* m : p->models)
                {
                    d_stdout(">>>>>>>>>>>>>>>>> LOADING module %s : %s", p->slug.c_str(), m->slug.c_str());
                    rack::engine::Module* const module = m->createModule();
                    DISTRHO_SAFE_ASSERT_CONTINUE(module != nullptr);

                    rack::CardinalPluginModelHelper* const helper = dynamic_cast<rack::CardinalPluginModelHelper*>(m);
                    DISTRHO_SAFE_ASSERT_CONTINUE(helper != nullptr);

                    d_stdout(">>>>>>>>>>>>>>>>> LOADING moduleWidget %s : %s", p->slug.c_str(), m->slug.c_str());
                    rack::app::ModuleWidget* const moduleWidget = helper->createModuleWidget(module);
                    DISTRHO_SAFE_ASSERT_CONTINUE(moduleWidget != nullptr);

                    d_stdout(">>>>>>>>>>>>>>>>> ADDING TO ENGINE %s : %s", p->slug.c_str(), m->slug.c_str());
                    context->engine->addModule(module);

                    d_stdout(">>>>>>>>>>>>>>>>> ADDING TO RACK VIEW %s : %s", p->slug.c_str(), m->slug.c_str());
                    context->scene->rack->addModuleAtMouse(moduleWidget);

                    for (int i=5; --i>=0;)
                        app.idle();

                    d_stdout(">>>>>>>>>>>>>>>>> REMOVING FROM RACK VIEW %s : %s", p->slug.c_str(), m->slug.c_str());
                    context->scene->rack->removeModule(moduleWidget);
                    app.idle();

                    d_stdout(">>>>>>>>>>>>>>>>> DELETING module + moduleWidget %s : %s", p->slug.c_str(), m->slug.c_str());
                    delete moduleWidget;

                    app.idle();
                }
            }

            inSelfTest = false;
        }
       #endif

       #if defined(DISTRHO_OS_WASM) && ! CARDINAL_VARIANT_MINI
        if (counterForFirstIdlePoint >= 0 && ++counterForFirstIdlePoint == 30)
        {
            counterForFirstIdlePoint = -1;

            if (rack::patchStorageSlug != nullptr)
            {
                std::string url("/patchstorage.php?slug=");
                url += rack::patchStorageSlug;
                std::free(rack::patchStorageSlug);
                rack::patchStorageSlug = nullptr;

                emscripten_async_wget(url.c_str(), context->patch->templatePath.c_str(),
                                      downloadRemotePatchSucceeded, downloadRemotePatchFailed);
            }
            else if (rack::patchRemoteURL != nullptr)
            {
                std::string url("/patchurl.php?url=");
                url += rack::patchRemoteURL;
                std::free(rack::patchRemoteURL);
                rack::patchRemoteURL = nullptr;

                emscripten_async_wget(url.c_str(), context->patch->templatePath.c_str(),
                                      downloadRemotePatchSucceeded, downloadRemotePatchFailed);
            }
        }
       #endif

        if (filebrowserhandle != nullptr && fileBrowserIdle(filebrowserhandle))
        {
            {
                const char* const path = fileBrowserGetPath(filebrowserhandle);

                const ScopedContext sc(this);
                filebrowseraction(path != nullptr ? strdup(path) : nullptr);
            }

            fileBrowserClose(filebrowserhandle);
            filebrowseraction = nullptr;
            filebrowserhandle = nullptr;
        }

       #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
        {
            const ScopedContext sc(this);
            for (uint32_t i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS;++i)
                context->dataOuts[i][0] = 0.f;
            ++context->processCounter;
            context->engine->stepBlock(1);
        }
       #endif

        if (windowParameters.rateLimit != 0 && ++rateLimitStep % (windowParameters.rateLimit * 2))
            return;

        rateLimitStep = 0;
        repaint();
    }

    void WindowParametersChanged(const WindowParameterList param, float value) override
    {
        float mult = 1.0f;

        switch (param)
        {
        case kWindowParameterShowTooltips:
            windowParameters.tooltips = value > 0.5f;
            break;
        case kWindowParameterCableOpacity:
            mult = 100.0f;
            windowParameters.cableOpacity = value;
            break;
        case kWindowParameterCableTension:
            mult = 100.0f;
            windowParameters.cableTension = value;
            break;
        case kWindowParameterRackBrightness:
            mult = 100.0f;
            windowParameters.rackBrightness = value;
            break;
        case kWindowParameterHaloBrightness:
            mult = 100.0f;
            windowParameters.haloBrightness = value;
            break;
        case kWindowParameterKnobMode:
            switch (static_cast<int>(value + 0.5f))
            {
            case rack::settings::KNOB_MODE_LINEAR:
                value = 0;
                windowParameters.knobMode = rack::settings::KNOB_MODE_LINEAR;
                break;
            case rack::settings::KNOB_MODE_ROTARY_ABSOLUTE:
                value = 1;
                windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_ABSOLUTE;
                break;
            case rack::settings::KNOB_MODE_ROTARY_RELATIVE:
                value = 2;
                windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_RELATIVE;
                break;
            }
            break;
        case kWindowParameterWheelKnobControl:
            windowParameters.knobScroll = value > 0.5f;
            break;
        case kWindowParameterWheelSensitivity:
            mult = 1000.0f;
            windowParameters.knobScrollSensitivity = value;
            break;
        case kWindowParameterLockModulePositions:
            windowParameters.lockModules = value > 0.5f;
            break;
        case kWindowParameterUpdateRateLimit:
            windowParameters.rateLimit = static_cast<int>(value + 0.5f);
            rateLimitStep = 0;
            break;
        case kWindowParameterBrowserSort:
            windowParameters.browserSort = static_cast<int>(value + 0.5f);
            break;
        case kWindowParameterBrowserZoom:
            windowParameters.browserZoom = value;
            value = std::pow(2.f, value) * 100.0f;
            break;
        case kWindowParameterInvertZoom:
            windowParameters.invertZoom = value > 0.5f;
            break;
        case kWindowParameterSqueezeModulePositions:
            windowParameters.squeezeModules = value > 0.5f;
            break;
        default:
            return;
        }

        setParameterValue(kCardinalParameterStartWindow + param, value * mult);
    }

protected:
   /* --------------------------------------------------------------------------------------------------------
    * DSP/Plugin Callbacks */

   /**
      A parameter has changed on the plugin side.
      This is called by the host to inform the UI about parameter changes.
    */
    void parameterChanged(const uint32_t index, const float value) override
    {
        // host mapped parameters
        if (index < kCardinalParameterCountAtModules)
        {
           #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
            context->parameters[index] = value;
           #endif
            return;
        }

        // bypass
        if (index == kCardinalParameterBypass)
        {
           #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
            context->bypassed = value > 0.5f;
           #endif
            return;
        }

        if (index < kCardinalParameterCountAtWindow)
        {
            switch (index - kCardinalParameterStartWindow)
            {
            case kWindowParameterShowTooltips:
                windowParameters.tooltips = value > 0.5f;
                break;
            case kWindowParameterCableOpacity:
                windowParameters.cableOpacity = value / 100.0f;
                break;
            case kWindowParameterCableTension:
                windowParameters.cableTension = value / 100.0f;
                break;
            case kWindowParameterRackBrightness:
                windowParameters.rackBrightness = value / 100.0f;
                break;
            case kWindowParameterHaloBrightness:
                windowParameters.haloBrightness = value / 100.0f;
                break;
            case kWindowParameterKnobMode:
                switch (static_cast<int>(value + 0.5f))
                {
                case 0:
                    windowParameters.knobMode = rack::settings::KNOB_MODE_LINEAR;
                    break;
                case 1:
                    windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_ABSOLUTE;
                    break;
                case 2:
                    windowParameters.knobMode = rack::settings::KNOB_MODE_ROTARY_RELATIVE;
                    break;
                }
                break;
            case kWindowParameterWheelKnobControl:
                windowParameters.knobScroll = value > 0.5f;
                break;
            case kWindowParameterWheelSensitivity:
                windowParameters.knobScrollSensitivity = value / 1000.0f;
                break;
            case kWindowParameterLockModulePositions:
                windowParameters.lockModules = value > 0.5f;
                break;
            case kWindowParameterUpdateRateLimit:
                windowParameters.rateLimit = static_cast<int>(value + 0.5f);
                rateLimitStep = 0;
                break;
            case kWindowParameterBrowserSort:
                windowParameters.browserSort = static_cast<int>(value + 0.5f);
                break;
            case kWindowParameterBrowserZoom:
                // round up to nearest valid value
                {
                    float rvalue = value - 1.0f;

                    if (rvalue <= 25.0f)
                        rvalue = -2.0f;
                    else if (rvalue <= 35.0f)
                        rvalue = -1.5f;
                    else if (rvalue <= 50.0f)
                        rvalue = -1.0f;
                    else if (rvalue <= 71.0f)
                        rvalue = -0.5f;
                    else if (rvalue <= 100.0f)
                        rvalue = 0.0f;
                    else if (rvalue <= 141.0f)
                        rvalue = 0.5f;
                    else if (rvalue <= 200.0f)
                        rvalue = 1.0f;
                    else
                        rvalue = 0.0f;

                    windowParameters.browserZoom = rvalue;
                }
                break;
            case kWindowParameterInvertZoom:
                windowParameters.invertZoom = value > 0.5f;
                break;
            case kWindowParameterSqueezeModulePositions:
                windowParameters.squeezeModules = value > 0.5f;
                break;
            default:
                return;
            }

            WindowParametersSetValues(context->window, windowParameters);
            return;
        }

       #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
        if (index < kCardinalParameterCountAtMiniBuffers)
        {
            float* const buffer = *const_cast<float**>(&context->dataIns[index - kCardinalParameterStartMiniBuffers]);
            buffer[0] = value;
            return;
        }

        switch (index)
        {
        case kCardinalParameterMiniTimeFlags: {
            const int32_t flags = static_cast<int32_t>(value + 0.5f);
            context->playing = flags & 0x1;
            context->bbtValid = flags & 0x2;
            context->reset = flags & 0x4;
            return;
        }
        case kCardinalParameterMiniTimeBar:
            context->bar = static_cast<int32_t>(value + 0.5f);
            return;
        case kCardinalParameterMiniTimeBeat:
            context->beat = static_cast<int32_t>(value + 0.5f);
            return;
        case kCardinalParameterMiniTimeBeatsPerBar:
            context->beatsPerBar = static_cast<int32_t>(value + 0.5f);
            return;
        case kCardinalParameterMiniTimeBeatType:
            context->beatType = static_cast<int32_t>(value + 0.5f);
            context->ticksPerClock = context->ticksPerBeat / context->beatType;
            context->tickClock = std::fmod(context->tick, context->ticksPerClock);
            return;
        case kCardinalParameterMiniTimeFrame:
            context->frame = static_cast<uint64_t>(value * context->sampleRate + 0.5f);
            return;
        case kCardinalParameterMiniTimeBarStartTick:
            context->barStartTick = value;
            return;
        case kCardinalParameterMiniTimeBeatsPerMinute:
            context->beatsPerMinute = value;
            context->ticksPerFrame = 1.0 / (60.0 * context->sampleRate / context->beatsPerMinute / context->ticksPerBeat);
            return;
        case kCardinalParameterMiniTimeTick:
            context->tick = value;
            context->tickClock = std::fmod(context->tick, context->ticksPerClock);
            return;
        case kCardinalParameterMiniTimeTicksPerBeat:
            context->ticksPerBeat = value;
            context->ticksPerClock = context->ticksPerBeat / context->beatType;
            context->ticksPerFrame = 1.0 / (60.0 * context->sampleRate / context->beatsPerMinute / context->ticksPerBeat);
            context->tickClock = std::fmod(context->tick, context->ticksPerClock);
            return;
        }
       #endif
    }

    void stateChanged(const char* const key, const char* const value) override
    {
       #if CARDINAL_VARIANT_MINI && ! DISTRHO_PLUGIN_WANT_DIRECT_ACCESS
        if (std::strcmp(key, "patch") == 0)
        {
            if (fAutosavePath.empty())
                return;

            rack::system::removeRecursively(fAutosavePath);
            rack::system::createDirectories(fAutosavePath);

            FILE* const f = std::fopen(rack::system::join(fAutosavePath, "patch.json").c_str(), "w");
            DISTRHO_SAFE_ASSERT_RETURN(f != nullptr,);

            std::fwrite(value, std::strlen(value), 1, f);
            std::fclose(f);

            const ScopedContext sc(this);

            try {
                context->patch->loadAutosave();
            } catch(const rack::Exception& e) {
                d_stderr(e.what());
            } DISTRHO_SAFE_EXCEPTION_RETURN("stateChanged loadAutosave",);

            return;
        }
       #endif

        if (std::strcmp(key, "windowSize") == 0)
        {
            int width = 0;
            int height = 0;
            std::sscanf(value, "%d:%d", &width, &height);

            if (width > 0 && height > 0)
            {
                const double scaleFactor = getScaleFactor();
                setSize(width * scaleFactor, height * scaleFactor);
            }

            return;
        }
    }

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

    static int glfwMods(const uint mod) noexcept
    {
        int mods = 0;

        if (mod & kModifierControl)
            mods |= GLFW_MOD_CONTROL;
        if (mod & kModifierShift)
            mods |= GLFW_MOD_SHIFT;
        if (mod & kModifierAlt)
            mods |= GLFW_MOD_ALT;
        if (mod & kModifierSuper)
            mods |= GLFW_MOD_SUPER;

        /*
        if (glfwGetKey(win, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS)
            mods |= GLFW_MOD_SHIFT;
        if (glfwGetKey(win, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS)
            mods |= GLFW_MOD_CONTROL;
        if (glfwGetKey(win, GLFW_KEY_LEFT_ALT) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS)
            mods |= GLFW_MOD_ALT;
        if (glfwGetKey(win, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS || glfwGetKey(win, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS)
            mods |= GLFW_MOD_SUPER;
        */

        return mods;
    }

    bool onMouse(const MouseEvent& ev) override
    {
       #ifdef DPF_RUNTIME_TESTING
        if (inSelfTest) return false;
       #endif

        if (ev.press)
            getWindow().focus();

        const int action = ev.press ? GLFW_PRESS : GLFW_RELEASE;
        int mods = glfwMods(ev.mod);

        int button;

        switch (ev.button)
        {
        case kMouseButtonLeft: button = GLFW_MOUSE_BUTTON_LEFT;   break;
        case kMouseButtonRight: button = GLFW_MOUSE_BUTTON_RIGHT;  break;
        case kMouseButtonMiddle: button = GLFW_MOUSE_BUTTON_MIDDLE; break;
        default:
            button = ev.button;
            break;
        }

       #ifdef DISTRHO_OS_MAC
        // Remap Ctrl-left click to right click on macOS
        if (button == GLFW_MOUSE_BUTTON_LEFT && (mods & RACK_MOD_MASK) == GLFW_MOD_CONTROL) {
            button = GLFW_MOUSE_BUTTON_RIGHT;
            mods &= ~GLFW_MOD_CONTROL;
        }
        // Remap Ctrl-shift-left click to middle click on macOS
        if (button == GLFW_MOUSE_BUTTON_LEFT && (mods & RACK_MOD_MASK) == (GLFW_MOD_CONTROL | GLFW_MOD_SHIFT)) {
            button = GLFW_MOUSE_BUTTON_MIDDLE;
            mods &= ~(GLFW_MOD_CONTROL | GLFW_MOD_SHIFT);
        }
       #endif

        const ScopedContext sc(this, mods);
        return context->event->handleButton(lastMousePos, button, action, mods);
    }

    bool onMotion(const MotionEvent& ev) override
    {
       #ifdef DPF_RUNTIME_TESTING
        if (inSelfTest) return false;
       #endif

        const rack::math::Vec mousePos = rack::math::Vec(ev.pos.getX(), ev.pos.getY()).div(getScaleFactor()).round();
        const rack::math::Vec mouseDelta = mousePos.minus(lastMousePos);

        lastMousePos = mousePos;

        const ScopedContext sc(this, glfwMods(ev.mod));
        return context->event->handleHover(mousePos, mouseDelta);
    }

    bool onScroll(const ScrollEvent& ev) override
    {
       #ifdef DPF_RUNTIME_TESTING
        if (inSelfTest) return false;
       #endif

        const rack::math::Vec scrollDelta = rack::math::Vec(-ev.delta.getX(), ev.delta.getY()) * 50 ;

        const int mods = glfwMods(ev.mod);
        const ScopedContext sc(this, mods);
        return context->event->handleScroll(lastMousePos, scrollDelta);
    }

    bool onCharacterInput(const CharacterInputEvent& ev) override
    {
       #ifdef DPF_RUNTIME_TESTING
        if (inSelfTest) return false;
       #endif

        if (ev.character < ' ' || ev.character >= kKeyDelete)
            return false;

        const int mods = glfwMods(ev.mod);
        const ScopedContext sc(this, mods);
        return context->event->handleText(lastMousePos, ev.character);
    }

    bool onKeyboard(const KeyboardEvent& ev) override
    {
       #ifdef DPF_RUNTIME_TESTING
        if (inSelfTest) return false;
       #endif

        const int action = ev.press ? GLFW_PRESS : GLFW_RELEASE;
        const int mods = glfwMods(ev.mod);

        int key;
        switch (ev.key)
        {
        case kKeyTab: key = GLFW_KEY_TAB; break;
        case kKeyBackspace: key = GLFW_KEY_BACKSPACE; break;
        case kKeyEnter: key = GLFW_KEY_ENTER; break;
        case kKeyEscape: key = GLFW_KEY_ESCAPE; break;
        case kKeyDelete: key = GLFW_KEY_DELETE; break;
        case kKeySpace: key = GLFW_KEY_SPACE; break;
        case kKeyF1: key = GLFW_KEY_F1; break;
        case kKeyF2: key = GLFW_KEY_F2; break;
        case kKeyF3: key = GLFW_KEY_F3; break;
        case kKeyF4: key = GLFW_KEY_F4; break;
        case kKeyF5: key = GLFW_KEY_F5; break;
        case kKeyF6: key = GLFW_KEY_F6; break;
        case kKeyF7: key = GLFW_KEY_F7; break;
        case kKeyF8: key = GLFW_KEY_F8; break;
        case kKeyF9: key = GLFW_KEY_F9; break;
        case kKeyF10: key = GLFW_KEY_F10; break;
        case kKeyF11: key = GLFW_KEY_F11; break;
        case kKeyF12: key = GLFW_KEY_F12; break;
        case kKeyPageUp: key = GLFW_KEY_PAGE_UP; break;
        case kKeyPageDown: key = GLFW_KEY_PAGE_DOWN; break;
        case kKeyEnd: key = GLFW_KEY_END; break;
        case kKeyHome: key = GLFW_KEY_HOME; break;
        case kKeyLeft: key = GLFW_KEY_LEFT; break;
        case kKeyUp: key = GLFW_KEY_UP; break;
        case kKeyRight: key = GLFW_KEY_RIGHT; break;
        case kKeyDown: key = GLFW_KEY_DOWN; break;
        case kKeyPrintScreen: key = GLFW_KEY_PRINT_SCREEN; break;
        case kKeyInsert: key = GLFW_KEY_INSERT; break;
        case kKeyPause: key = GLFW_KEY_PAUSE; break;
        case kKeyMenu: key = GLFW_KEY_MENU; break;
        case kKeyNumLock: key = GLFW_KEY_NUM_LOCK; break;
        case kKeyScrollLock: key = GLFW_KEY_SCROLL_LOCK; break;
        case kKeyCapsLock: key = GLFW_KEY_CAPS_LOCK; break;
        case kKeyShiftL: key = GLFW_KEY_LEFT_SHIFT; break;
        case kKeyShiftR: key = GLFW_KEY_RIGHT_SHIFT; break;
        case kKeyControlL: key = GLFW_KEY_LEFT_CONTROL; break;
        case kKeyControlR: key = GLFW_KEY_RIGHT_CONTROL; break;
        case kKeyAltL: key = GLFW_KEY_LEFT_ALT; break;
        case kKeyAltR: key = GLFW_KEY_RIGHT_ALT; break;
        case kKeySuperL: key = GLFW_KEY_LEFT_SUPER; break;
        case kKeySuperR: key = GLFW_KEY_RIGHT_SUPER; break;
        case kKeyPad0: key = GLFW_KEY_KP_0; break;
        case kKeyPad1: key = GLFW_KEY_KP_1; break;
        case kKeyPad2: key = GLFW_KEY_KP_2; break;
        case kKeyPad3: key = GLFW_KEY_KP_3; break;
        case kKeyPad4: key = GLFW_KEY_KP_4; break;
        case kKeyPad5: key = GLFW_KEY_KP_5; break;
        case kKeyPad6: key = GLFW_KEY_KP_6; break;
        case kKeyPad7: key = GLFW_KEY_KP_7; break;
        case kKeyPad8: key = GLFW_KEY_KP_8; break;
        case kKeyPad9: key = GLFW_KEY_KP_9; break;
        case kKeyPadEnter: key = GLFW_KEY_KP_ENTER; break;
        /* undefined in glfw
        case kKeyPadPageUp:
        case kKeyPadPageDown:
        case kKeyPadEnd:
        case kKeyPadHome:
        case kKeyPadLeft:
        case kKeyPadUp:
        case kKeyPadRight:
        case kKeyPadDown:
        case kKeyPadClear:
        case kKeyPadInsert:
        case kKeyPadDelete:
        */
        case kKeyPadEqual: key = GLFW_KEY_KP_EQUAL; break;
        case kKeyPadMultiply: key = GLFW_KEY_KP_MULTIPLY; break;
        case kKeyPadAdd: key = GLFW_KEY_KP_ADD; break;
        /* undefined in glfw
        case kKeyPadSeparator:
        */
        case kKeyPadSubtract: key = GLFW_KEY_KP_SUBTRACT; break;
        case kKeyPadDecimal: key = GLFW_KEY_KP_DECIMAL; break;
        case kKeyPadDivide: key = GLFW_KEY_KP_DIVIDE; break;
        default:
            // glfw expects uppercase
            if (ev.key >= 'a' && ev.key <= 'z')
                key = ev.key - ('a' - 'A');
            else
                key = ev.key;
            break;
        }

        const ScopedContext sc(this, mods);
        return context->event->handleKey(lastMousePos, key, ev.keycode, action, mods);
    }

    void onResize(const ResizeEvent& ev) override
    {
        UI::onResize(ev);

        if (context->window != nullptr)
            WindowSetInternalSize(context->window, rack::math::Vec(ev.size.getWidth(), ev.size.getHeight()));

        const double scaleFactor = getScaleFactor();
        const int width = static_cast<int>(ev.size.getWidth() / scaleFactor + 0.5);
        const int height = static_cast<int>(ev.size.getHeight() / scaleFactor + 0.5);

        char sizeString[64] = {};
        std::snprintf(sizeString, sizeof(sizeString) - 1, "%d:%d", width, height);
        setState("windowSize", sizeString);

        if (rack::isStandalone())
            rack::settings::windowSize = rack::math::Vec(width, height);
    }

    void uiFocus(const bool focus, CrossingMode) override
    {
       #ifdef DPF_RUNTIME_TESTING
        if (inSelfTest) return;
       #endif

        if (!focus)
        {
            const ScopedContext sc(this, 0);
            context->event->handleLeave();
        }
    }

    void uiFileBrowserSelected(const char* const filename) override
    {
        if (filename == nullptr)
            return;

        rack::contextSet(context);
        WindowParametersRestore(context->window);

        std::string sfilename = filename;

        if (saving)
        {
            const bool uncompressed = savingUncompressed;
            savingUncompressed = false;

            if (rack::system::getExtension(sfilename) != ".vcv")
                sfilename += ".vcv";

            try {
                if (uncompressed)
                {
                    context->engine->prepareSave();

                    if (json_t* const rootJ = context->patch->toJson())
                    {
                        if (FILE* const file = std::fopen(sfilename.c_str(), "w"))
                        {
                            json_dumpf(rootJ, file, JSON_INDENT(2));
                            std::fclose(file);
                        }
                        json_decref(rootJ);
                    }
                }
                else
                {
                    context->patch->save(sfilename);
                }
            }
            catch (rack::Exception& e) {
                std::string message = rack::string::f("Could not save patch: %s", e.what());
                asyncDialog::create(message.c_str());
                return;
            }
        }
        else
        {
            try {
                context->patch->load(sfilename);
            } catch (rack::Exception& e) {
                std::string message = rack::string::f("Could not load patch: %s", e.what());
                asyncDialog::create(message.c_str());
                return;
            }
        }

        context->patch->path = sfilename;
        context->patch->pushRecentPath(sfilename);
        context->history->setSaved();

       #ifdef DISTRHO_OS_WASM
        rack::syncfs();
       #else
        rack::settings::save();
       #endif
    }

#if 0
    void uiReshape(const uint width, const uint height) override
    {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glOrtho(0.0, width, 0.0, height, -1.0, 1.0);
        glViewport(0, 0, width, height);
        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();
    }
#endif

private:
   /**
      Set our UI class as non-copyable and add a leak detector just in case.
    */
    DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CardinalUI)
};

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

UI* createUI()
{
    return new CardinalUI();
}

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

END_NAMESPACE_DISTRHO
