/*
 * Carla Native Plugins
 * Copyright (C) 2013-2019 Filipe Coelho <falktx@falktx.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of
 * the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * For a full copy of the GNU General Public License see the doc/GPL.txt file.
 */

#ifndef HAVE_PYQT
# error This file should not be built
#endif

#include "CarlaLv2Utils.hpp"
#include "CarlaPipeUtils.hpp"
#include "CarlaScopeUtils.hpp"

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

class NativePluginUI : public LV2_External_UI_Widget_Compat
{
public:
    NativePluginUI(LV2UI_Write_Function writeFunction, LV2UI_Controller controller,
                   LV2UI_Widget* widget, const LV2_Feature* const* features)
        : fUridMap(nullptr),
          fUridUnmap(nullptr),
          fUridTranser(0),
          fUridTranser2(0),
          fUI()
    {
        run  = extui_run;
        show = extui_show;
        hide = extui_hide;

        fUI.writeFunction = writeFunction;
        fUI.controller = controller;

        const LV2_URID_Map*   uridMap   = nullptr;
        const LV2_URID_Unmap* uridUnmap = nullptr;

        for (int i=0; features[i] != nullptr; ++i)
        {
            /**/ 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_URID__unmap) == 0)
                uridUnmap = (const LV2_URID_Unmap*)features[i]->data;
        }

        if (uridMap == nullptr)
        {
            carla_stderr("Host doesn't provide urid-map feature");
            return;
        }

        fUridMap = uridMap;
        fUridUnmap = uridUnmap;
        fUridTranser = uridMap->map(uridMap->handle, LV2_ATOM__eventTransfer);
        fUridTranser2 = uridMap->map(uridMap->handle, "urn:carla:transmitEv");

        // ---------------------------------------------------------------
        // see if the host supports external-ui

        for (int i=0; features[i] != nullptr; ++i)
        {
            if (std::strcmp(features[i]->URI, LV2_EXTERNAL_UI__Host) == 0 ||
                std::strcmp(features[i]->URI, LV2_EXTERNAL_UI_DEPRECATED_URI) == 0)
            {
                fUI.host = (const LV2_External_UI_Host*)features[i]->data;
                break;
            }
        }

        if (fUI.host != nullptr)
        {
            fUI.name = carla_strdup(fUI.host->plugin_human_id);
            *widget = (LV2_External_UI_Widget_Compat*)this;
            return;
        }

        // ---------------------------------------------------------------
        // no external-ui support, use showInterface

        for (int i=0; features[i] != nullptr; ++i)
        {
            if (std::strcmp(features[i]->URI, LV2_OPTIONS__options) != 0)
                continue;

            const LV2_Options_Option* const options((const LV2_Options_Option*)features[i]->data);
            CARLA_SAFE_ASSERT_BREAK(options != nullptr);

            for (int j=0; options[j].key != 0; ++j)
            {
                if (options[j].key != uridMap->map(uridMap->handle, LV2_UI__windowTitle))
                    continue;

                const char* const title((const char*)options[j].value);
                CARLA_SAFE_ASSERT_BREAK(title != nullptr && title[0] != '\0');

                fUI.name = carla_strdup(title);
                break;
            }
            break;
        }

        if (fUI.name == nullptr)
            fUI.name = carla_strdup("Carla");

        *widget = nullptr;
    }

    ~NativePluginUI()
    {
        if (fUI.isVisible)
            writeAtomMessage("quit");

        fUI.host = nullptr;
        fUI.writeFunction = nullptr;
        fUI.controller = nullptr;
    }

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

    void lv2ui_port_event(uint32_t portIndex, uint32_t bufferSize, uint32_t format, const void* buffer)
    {
        CARLA_SAFE_ASSERT_RETURN(buffer != nullptr,);

        if (format == 0)
        {
            char msg[128];
            const float* const valuePtr = (const float*)buffer;

            {
                const CarlaScopedLocale csl;
                std::snprintf(msg, 127, "control %u %.12g", portIndex, static_cast<double>(*valuePtr));
            }

            msg[127] = '\0';

            writeAtomMessage(msg);
            return;
        }

        if (format == fUridTranser)
        {
            CARLA_SAFE_ASSERT_RETURN(bufferSize > sizeof(LV2_Atom),);

            const LV2_Atom* const atom = (const LV2_Atom*)buffer;

            if (atom->type == fUridTranser2)
            {
                const char* const msg = (const char*)(atom + 1);

                if (std::strcmp(msg, "quit") == 0)
                {
                    handleUiClosed();
                }

                return;
            }
        }

        if (fUridUnmap != nullptr)
        {
            carla_stdout("lv2ui_port_event %u %u %u:%s %p",
                         portIndex, bufferSize, format, fUridUnmap->unmap(fUridUnmap->handle, format), buffer);
        }
    }

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

    void lv2ui_select_program(uint32_t bank, uint32_t program) const
    {
        char msg[128];
        std::snprintf(msg, 127, "program %u %u", bank, program);
        msg[127] = '\0';

        writeAtomMessage(msg);
    }

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

    int lv2ui_idle() const
    {
        if (! fUI.isVisible)
            return 1;

        handleUiRun();
        return 0;
    }

    int lv2ui_show()
    {
        handleUiShow();
        return 0;
    }

    int lv2ui_hide()
    {
        handleUiHide();
        return 0;
    }

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

protected:
    void handleUiShow()
    {
        writeAtomMessage("show");
        fUI.isVisible = true;
    }

    void handleUiHide()
    {
        if (fUI.isVisible)
        {
            fUI.isVisible = false;
            writeAtomMessage("hide");
        }
    }

    void handleUiRun() const
    {
        if (fUI.isVisible)
            writeAtomMessage("idle");
    }

    void handleUiClosed()
    {
        fUI.isVisible = false;

        if (fUI.host != nullptr && fUI.host->ui_closed != nullptr && fUI.controller != nullptr)
            fUI.host->ui_closed(fUI.controller);

        fUI.host = nullptr;
        fUI.writeFunction = nullptr;
        fUI.controller = nullptr;
    }

    bool writeAtomMessage(const char* const msg) const
    {
        CARLA_SAFE_ASSERT_RETURN(fUI.writeFunction != nullptr, false);
        CARLA_SAFE_ASSERT_RETURN(fUridTranser != 0, false);
        CARLA_SAFE_ASSERT_RETURN(fUridTranser2 != 0, false);

#ifdef DEBUG
        if (std::strcmp(msg, "idle") != 0) {
            carla_debug("writeAtomMessage(%s)", msg);
        }
#endif

        const size_t msgSize  = std::strlen(msg)+1;
        const size_t atomSize = sizeof(LV2_Atom) + msgSize;

        if (atomSize <= 128)
        {
            char atomBuf[atomSize];
            carla_zeroChars(atomBuf, atomSize);

            LV2_Atom* const atom = (LV2_Atom*)atomBuf;
            atom->size = static_cast<uint32_t>(msgSize);
            atom->type = fUridTranser2;
            std::memcpy(atomBuf+sizeof(LV2_Atom), msg, msgSize);

            fUI.writeFunction(fUI.controller, 0, static_cast<uint32_t>(atomSize), fUridTranser, atomBuf);
        }
        else
        {
            char* const atomBuf = new char[atomSize];
            carla_zeroChars(atomBuf, atomSize);

            LV2_Atom* const atom = (LV2_Atom*)atomBuf;
            atom->size = static_cast<uint32_t>(msgSize);
            atom->type = fUridTranser2;
            std::memcpy(atomBuf+sizeof(LV2_Atom), msg, msgSize);

            fUI.writeFunction(fUI.controller, 0, static_cast<uint32_t>(atomSize), fUridTranser, atomBuf);

            delete[] atomBuf;
        }

        return true;
    }

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

private:
    const LV2_URID_Map* fUridMap;
    const LV2_URID_Unmap* fUridUnmap;
    LV2_URID fUridTranser, fUridTranser2;

    struct UI {
        const LV2_External_UI_Host* host;
        LV2UI_Write_Function writeFunction;
        LV2UI_Controller controller;
        const char* name;
        bool isVisible;

        UI()
            : host(nullptr),
              writeFunction(nullptr),
              controller(nullptr),
              name(nullptr),
              isVisible(false) {}

        ~UI()
        {
            if (name != nullptr)
            {
                delete[] name;
                name = nullptr;
            }
        }

        CARLA_DECLARE_NON_COPY_STRUCT(UI);
    } fUI;

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

    #define handlePtr ((NativePluginUI*)handle)

    static void extui_run(LV2_External_UI_Widget_Compat* handle)
    {
        handlePtr->handleUiRun();
    }

    static void extui_show(LV2_External_UI_Widget_Compat* handle)
    {
        carla_debug("extui_show(%p)", handle);
        handlePtr->handleUiShow();
    }

    static void extui_hide(LV2_External_UI_Widget_Compat* handle)
    {
        carla_debug("extui_hide(%p)", handle);
        handlePtr->handleUiHide();
    }

    #undef handlePtr

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

    CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(NativePluginUI)
};

// -----------------------------------------------------------------------
// LV2 UI descriptor functions

static LV2UI_Handle lv2ui_instantiate(const LV2UI_Descriptor*, const char*, const char*,
                                      LV2UI_Write_Function writeFunction, LV2UI_Controller controller,
                                      LV2UI_Widget* widget, const LV2_Feature* const* features)
{
    carla_debug("lv2ui_instantiate(..., %p, %p, %p)", writeFunction, controller, widget, features);

    NativePluginUI* const ui = new NativePluginUI(writeFunction, controller, widget, features);

    // TODO: check ok

    return (LV2UI_Handle)ui;
}

#define uiPtr ((NativePluginUI*)ui)

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

static void lv2ui_cleanup(LV2UI_Handle ui)
{
    carla_debug("lv2ui_cleanup(%p)", ui);
    delete uiPtr;
}

static void lv2ui_select_program(LV2UI_Handle ui, uint32_t bank, uint32_t program)
{
    carla_debug("lv2ui_select_program(%p, %i, %i)", ui, bank, program);
    uiPtr->lv2ui_select_program(bank, program);
}

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

static int lv2ui_show(LV2UI_Handle ui)
{
    carla_debug("lv2ui_show(%p)", ui);
    return uiPtr->lv2ui_show();
}

static int lv2ui_hide(LV2UI_Handle ui)
{
    carla_debug("lv2ui_hide(%p)", ui);
    return uiPtr->lv2ui_hide();
}

static const void* lv2ui_extension_data(const char* uri)
{
    carla_stdout("lv2ui_extension_data(\"%s\")", uri);

    static const LV2UI_Idle_Interface uiidle = { lv2ui_idle };
    static const LV2UI_Show_Interface uishow = { lv2ui_show, lv2ui_hide };
    static const LV2_Programs_UI_Interface uiprograms = { lv2ui_select_program };

    if (std::strcmp(uri, LV2_UI__idleInterface) == 0)
        return &uiidle;
    if (std::strcmp(uri, LV2_UI__showInterface) == 0)
        return &uishow;
    if (std::strcmp(uri, LV2_PROGRAMS__UIInterface) == 0)
        return &uiprograms;

    return nullptr;
}

#undef uiPtr

// -----------------------------------------------------------------------
// Startup code

CARLA_EXPORT
const LV2UI_Descriptor* lv2ui_descriptor(uint32_t index)
{
    carla_debug("lv2ui_descriptor(%i)", index);

    static const LV2UI_Descriptor lv2UiExtDesc = {
    /* URI            */ "http://kxstudio.sf.net/carla/ui-bridge-ext",
    /* instantiate    */ lv2ui_instantiate,
    /* cleanup        */ lv2ui_cleanup,
    /* port_event     */ lv2ui_port_event,
    /* extension_data */ lv2ui_extension_data
    };

    return (index == 0) ? &lv2UiExtDesc : nullptr;
}

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

#include "CarlaPipeUtils.cpp"

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