/*
 * DISTRHO Plugin Framework (DPF)
 * Copyright (C) 2012-2022 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 "DistrhoPluginInternal.hpp"

#if DISTRHO_PLUGIN_HAS_UI
# include "DistrhoUIInternal.hpp"
#endif

#include "CarlaNative.hpp"

// TODO
#undef DISTRHO_PLUGIN_WANT_PARAMETER_VALUE_CHANGE_REQUEST
#define DISTRHO_PLUGIN_WANT_PARAMETER_VALUE_CHANGE_REQUEST 0

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

START_NAMESPACE_DISTRHO

#if ! DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
static constexpr const writeMidiFunc writeMidiCallback = nullptr;
#endif
#if ! DISTRHO_PLUGIN_WANT_PARAMETER_VALUE_CHANGE_REQUEST
static constexpr const requestParameterValueChangeFunc requestParameterValueChangeCallback = nullptr;
#endif
// TODO
static constexpr const updateStateValueFunc updateStateValueCallback = nullptr;

#if DISTRHO_PLUGIN_HAS_UI
// -----------------------------------------------------------------------
// Carla UI

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

class UICarla
{
public:
    UICarla(const NativeHostDescriptor* const host, PluginExporter* const plugin)
        : fHost(host),
          fUI(this, 0, plugin->getSampleRate(),
              editParameterCallback, setParameterCallback, setStateCallback, sendNoteCallback,
              nullptr, // window size
              nullptr, // TODO file request
              nullptr, // bundle path
              plugin->getInstancePointer())
    {
        fUI.setWindowTitle(host->uiName);

        if (host->uiParentId != 0)
            fUI.setWindowTransientWinId(host->uiParentId);
    }

    ~UICarla()
    {
        fUI.quit();
    }

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

    void carla_show(const bool yesNo)
    {
        fUI.setWindowVisible(yesNo);
    }

    bool carla_idle()
    {
        return fUI.plugin_idle();
    }

    void carla_setParameterValue(const uint32_t index, const float value)
    {
        fUI.parameterChanged(index, value);
    }

#if DISTRHO_PLUGIN_WANT_PROGRAMS
    void carla_setMidiProgram(const uint32_t realProgram)
    {
        fUI.programLoaded(realProgram);
    }
#endif

#if DISTRHO_PLUGIN_WANT_STATE
    void carla_setCustomData(const char* const key, const char* const value)
    {
        fUI.stateChanged(key, value);
    }
#endif

    void carla_setUiTitle(const char* const uiTitle)
    {
        fUI.setWindowTitle(uiTitle);
    }

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

protected:
    void handleEditParameter(const uint32_t, const bool)
    {
        // TODO
    }

    void handleSetParameterValue(const uint32_t rindex, const float value)
    {
        fHost->ui_parameter_changed(fHost->handle, rindex, value);
    }

#if DISTRHO_PLUGIN_WANT_STATE
    void handleSetState(const char* const key, const char* const value)
    {
        fHost->ui_custom_data_changed(fHost->handle, key, value);
    }
#endif

#if DISTRHO_PLUGIN_IS_SYNTH
    void handleSendNote(const uint8_t, const uint8_t, const uint8_t)
    {
        // TODO
    }
#endif

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

private:
    // Plugin stuff
    const NativeHostDescriptor* const fHost;

    // UI
    UIExporter fUI;

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

    #define handlePtr ((UICarla*)ptr)

    static void editParameterCallback(void* ptr, uint32_t index, bool started)
    {
        handlePtr->handleEditParameter(index, started);
    }

    static void setParameterCallback(void* ptr, uint32_t rindex, float value)
    {
        handlePtr->handleSetParameterValue(rindex, value);
    }

#if DISTRHO_PLUGIN_WANT_STATE
    static void setStateCallback(void* ptr, const char* key, const char* value)
    {
        handlePtr->handleSetState(key, value);
    }
#endif

#if DISTRHO_PLUGIN_IS_SYNTH
    static void sendNoteCallback(void* ptr, uint8_t channel, uint8_t note, uint8_t velocity)
    {
        handlePtr->handleSendNote(channel, note, velocity);
    }
#endif

    #undef handlePtr

    CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(UICarla)
};
#endif // DISTRHO_PLUGIN_HAS_UI

// -----------------------------------------------------------------------
// Carla Plugin

class PluginCarla : public NativePluginClass
{
public:
    PluginCarla(const NativeHostDescriptor* const host)
        : NativePluginClass(host),
          fPlugin(this, writeMidiCallback, requestParameterValueChangeCallback, updateStateValueCallback),
          fScalePointsCache(nullptr)
    {
#if DISTRHO_PLUGIN_HAS_UI
        fUiPtr = nullptr;
#endif
    }

    ~PluginCarla() override
    {
#if DISTRHO_PLUGIN_HAS_UI
        if (fUiPtr != nullptr)
        {
            delete fUiPtr;
            fUiPtr = nullptr;
        }
#endif

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

protected:
    // -------------------------------------------------------------------
    // Plugin parameter calls

    uint32_t getParameterCount() const override
    {
        return fPlugin.getParameterCount();
    }

    const NativeParameter* getParameterInfo(const uint32_t index) const override
    {
        CARLA_SAFE_ASSERT_RETURN(index < getParameterCount(), nullptr);

        static NativeParameter param;

        param.scalePointCount = 0;
        param.scalePoints = nullptr;

        {
            int      nativeParamHints = ::NATIVE_PARAMETER_IS_ENABLED;
            const uint32_t paramHints = fPlugin.getParameterHints(index);

            if (paramHints & kParameterIsAutomatable)
                nativeParamHints |= ::NATIVE_PARAMETER_IS_AUTOMABLE;
            if (paramHints & kParameterIsBoolean)
                nativeParamHints |= ::NATIVE_PARAMETER_IS_BOOLEAN;
            if (paramHints & kParameterIsInteger)
                nativeParamHints |= ::NATIVE_PARAMETER_IS_INTEGER;
            if (paramHints & kParameterIsLogarithmic)
                nativeParamHints |= ::NATIVE_PARAMETER_IS_LOGARITHMIC;
            if (paramHints & kParameterIsOutput)
                nativeParamHints |= ::NATIVE_PARAMETER_IS_OUTPUT;

            param.hints = static_cast<NativeParameterHints>(nativeParamHints);
        }

        param.name = fPlugin.getParameterName(index);
        param.unit = fPlugin.getParameterUnit(index);

        {
            const ParameterRanges& ranges(fPlugin.getParameterRanges(index));

            param.ranges.def = ranges.def;
            param.ranges.min = ranges.min;
            param.ranges.max = ranges.max;
        }

        {
            const ParameterEnumerationValues& enumValues(fPlugin.getParameterEnumValues(index));

            if (const uint32_t scalePointCount = enumValues.count)
            {
                NativeParameterScalePoint* const scalePoints = new NativeParameterScalePoint[scalePointCount];

                for (uint32_t i=0; i<scalePointCount; ++i)
                {
                    scalePoints[i].label = enumValues.values[i].label.buffer();
                    scalePoints[i].value = enumValues.values[i].value;
                }

                param.scalePoints     = scalePoints;
                param.scalePointCount = scalePointCount;

                if (enumValues.restrictedMode)
                    param.hints = static_cast<NativeParameterHints>(param.hints|::NATIVE_PARAMETER_USES_SCALEPOINTS);
            }
            else if (fScalePointsCache != nullptr)
            {
                delete[] fScalePointsCache;
                fScalePointsCache = nullptr;
            }
        }

        return &param;
    }

    float getParameterValue(const uint32_t index) const override
    {
        CARLA_SAFE_ASSERT_RETURN(index < getParameterCount(), 0.0f);

        return fPlugin.getParameterValue(index);
    }

    // -------------------------------------------------------------------
    // Plugin midi-program calls

#if DISTRHO_PLUGIN_WANT_PROGRAMS
    uint32_t getMidiProgramCount() const override
    {
        return fPlugin.getProgramCount();
    }

    const NativeMidiProgram* getMidiProgramInfo(const uint32_t index) const override
    {
        CARLA_SAFE_ASSERT_RETURN(index < getMidiProgramCount(), nullptr);

        static NativeMidiProgram midiProgram;

        midiProgram.bank    = index / 128;
        midiProgram.program = index % 128;
        midiProgram.name    = fPlugin.getProgramName(index);

        return &midiProgram;
    }
#endif

    // -------------------------------------------------------------------
    // Plugin state calls

    void setParameterValue(const uint32_t index, const float value) override
    {
        CARLA_SAFE_ASSERT_RETURN(index < getParameterCount(),);

        fPlugin.setParameterValue(index, value);
    }

#if DISTRHO_PLUGIN_WANT_PROGRAMS
    void setMidiProgram(const uint8_t, const uint32_t bank, const uint32_t program) override
    {
        const uint32_t realProgram(bank * 128 + program);

        CARLA_SAFE_ASSERT_RETURN(realProgram < getMidiProgramCount(),);

        fPlugin.loadProgram(realProgram);
    }
#endif

#if DISTRHO_PLUGIN_WANT_STATE
    void setCustomData(const char* const key, const char* const value) override
    {
        CARLA_SAFE_ASSERT_RETURN(key != nullptr && key[0] != '\0',);
        CARLA_SAFE_ASSERT_RETURN(value != nullptr,);

        fPlugin.setState(key, value);
    }
#endif

    // -------------------------------------------------------------------
    // Plugin process calls

    void activate() override
    {
        fPlugin.activate();
    }

    void deactivate() override
    {
        fPlugin.deactivate();
    }

#if DISTRHO_PLUGIN_WANT_MIDI_INPUT
    void process(const float* const* const inBuffer, float** const outBuffer, const uint32_t frames,
                 const NativeMidiEvent* const midiEvents, const uint32_t midiEventCount) override
    {
        MidiEvent realMidiEvents[midiEventCount];

        for (uint32_t i=0; i < midiEventCount; ++i)
        {
            const NativeMidiEvent& midiEvent(midiEvents[i]);
            MidiEvent& realMidiEvent(realMidiEvents[i]);

            realMidiEvent.frame = midiEvent.time;
            realMidiEvent.size  = midiEvent.size;

            uint8_t j=0;
            for (; j<midiEvent.size; ++j)
                realMidiEvent.data[j] = midiEvent.data[j];
            for (; j<midiEvent.size; ++j)
                realMidiEvent.data[j] = midiEvent.data[j];

            realMidiEvent.dataExt = nullptr;
        }

        fPlugin.run(const_cast<const float**>(inBuffer), outBuffer, frames, realMidiEvents, midiEventCount);
    }
#else
    void process(const float* const* const inBuffer, float** const outBuffer, const uint32_t frames,
                 const NativeMidiEvent* const, const uint32_t) override
    {
        fPlugin.run(const_cast<const float**>(inBuffer), outBuffer, frames);
    }
#endif

    // -------------------------------------------------------------------
    // Plugin UI calls

#if DISTRHO_PLUGIN_HAS_UI
    void uiShow(const bool show) override
    {
        if (show)
        {
            createUiIfNeeded();
            CARLA_SAFE_ASSERT_RETURN(fUiPtr != nullptr,);

            fUiPtr->carla_show(show);
        }
        else if (fUiPtr != nullptr)
        {
            delete fUiPtr;
            fUiPtr = nullptr;
        }
    }

    void uiIdle() override
    {
        CARLA_SAFE_ASSERT_RETURN(fUiPtr != nullptr,);

        if (! fUiPtr->carla_idle())
        {
            uiClosed();

            delete fUiPtr;
            fUiPtr = nullptr;
        }
    }

    void uiSetParameterValue(const uint32_t index, const float value) override
    {
        CARLA_SAFE_ASSERT_RETURN(fUiPtr != nullptr,);
        CARLA_SAFE_ASSERT_RETURN(index < getParameterCount(),);

        fUiPtr->carla_setParameterValue(index, value);
    }

# if DISTRHO_PLUGIN_WANT_PROGRAMS
    void uiSetMidiProgram(const uint8_t, const uint32_t bank, const uint32_t program) override
    {
        CARLA_SAFE_ASSERT_RETURN(fUiPtr != nullptr,);

        const uint32_t realProgram(bank * 128 + program);

        CARLA_SAFE_ASSERT_RETURN(realProgram < getMidiProgramCount(),);

        fUiPtr->carla_setMidiProgram(realProgram);
    }
# endif

# if DISTRHO_PLUGIN_WANT_STATE
    void uiSetCustomData(const char* const key, const char* const value) override
    {
        CARLA_SAFE_ASSERT_RETURN(fUiPtr != nullptr,);
        CARLA_SAFE_ASSERT_RETURN(key != nullptr && key[0] != '\0',);
        CARLA_SAFE_ASSERT_RETURN(value != nullptr,);

        fUiPtr->carla_setCustomData(key, value);
    }
# endif
#endif

    // -------------------------------------------------------------------
    // Plugin dispatcher calls

    void bufferSizeChanged(const uint32_t bufferSize) override
    {
        fPlugin.setBufferSize(bufferSize, true);
    }

    void sampleRateChanged(const double sampleRate) override
    {
        fPlugin.setSampleRate(sampleRate, true);
    }

#if DISTRHO_PLUGIN_HAS_UI
    void uiNameChanged(const char* const uiName) override
    {
        CARLA_SAFE_ASSERT_RETURN(fUiPtr != nullptr,);

        fUiPtr->carla_setUiTitle(uiName);
    }
#endif

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

private:
    PluginExporter fPlugin;
    mutable NativeParameterScalePoint* fScalePointsCache;

#if DISTRHO_PLUGIN_HAS_UI
    // UI
    UICarla* fUiPtr;

    void createUiIfNeeded()
    {
        if (fUiPtr == nullptr)
            fUiPtr = new UICarla(getHostHandle(), &fPlugin);
    }
#endif

#if DISTRHO_PLUGIN_WANT_MIDI_OUTPUT
    static bool writeMidiCallback(void* ptr, const MidiEvent& midiEvent)
    {
        if (midiEvent.size > 4)
            return;

        const NativeMidiEvent event = {
            midiEvent.frame, 0, midiEvent.size, midiEvent.data
        };

        return ((PluginCarla*)ptr)->fPlugin.writeMidiEvent(midiEvent);
    }
#endif

#if DISTRHO_PLUGIN_WANT_PARAMETER_VALUE_CHANGE_REQUEST
    bool requestParameterValueChange(const uint32_t index, const float value)
    {
        // TODO implementation
        return false;
    }

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

    CARLA_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginCarla)

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

public:
    static NativePluginHandle _instantiate(const NativeHostDescriptor* host)
    {
        d_nextBufferSize = host->get_buffer_size(host->handle);
        d_nextSampleRate = host->get_sample_rate(host->handle);
        return new PluginCarla(host);
    }

    static void _cleanup(NativePluginHandle handle)
    {
        delete (PluginCarla*)handle;
    }
};

END_NAMESPACE_DISTRHO

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