/*
 * DISTRHO Cardinal Plugin
 * Copyright (C) 2021-2022 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 <juce_audio_processors/juce_audio_processors.h>

#define createPlugin createStaticPlugin
#include "src/DistrhoPluginInternal.hpp"
#include "src/DistrhoUIInternal.hpp"

START_NAMESPACE_DISTRHO

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

class ParameterFromDPF : public juce::AudioProcessorParameter
{
    PluginExporter& plugin;
    const ParameterEnumerationValues& enumValues;
    const ParameterRanges& ranges;
    const uint32_t hints;
    const uint index;
    bool* const updatedPtr;
    mutable juce::StringArray dpfValueStrings;

public:
    ParameterFromDPF(PluginExporter& plugin_, const uint index_, bool* const updatedPtr_)
        : plugin(plugin_),
          enumValues(plugin_.getParameterEnumValues(index_)),
          ranges(plugin_.getParameterRanges(index_)),
          hints(plugin_.getParameterHints(index_)),
          index(index_),
          updatedPtr(updatedPtr_) {}

    void setValueNotifyingHostFromDPF(const float newValue)
    {
        setValueNotifyingHost(ranges.getNormalizedValue(newValue));
        *updatedPtr = false;
    }

protected:
    float getValue() const override
    {
        return ranges.getNormalizedValue(plugin.getParameterValue(index));
    }

    void setValue(const float newValue) override
    {
        *updatedPtr = true;
        plugin.setParameterValue(index, ranges.getUnnormalizedValue(newValue));
    }

    float getDefaultValue() const override
    {
        return ranges.getNormalizedValue(plugin.getParameterDefault(index));
    }

    juce::String getName(const int maximumStringLength) const override
    {
        if (maximumStringLength <= 0)
            return juce::String(plugin.getParameterName(index).buffer());

        return juce::String(plugin.getParameterName(index).buffer(), static_cast<size_t>(maximumStringLength));
    }

    juce::String getLabel() const override
    {
        return plugin.getParameterUnit(index).buffer();
    }

    int getNumSteps() const override
    {
        if (hints & kParameterIsBoolean)
            return 2;

        if (enumValues.restrictedMode)
            return enumValues.count;

        if (hints & kParameterIsInteger)
            return ranges.max - ranges.min;

        return juce::AudioProcessorParameter::getNumSteps();
    }

    bool isDiscrete() const override
    {
        if (hints & (kParameterIsBoolean|kParameterIsInteger))
            return true;

        if (enumValues.restrictedMode)
            return true;

        return false;
    }

    bool isBoolean() const override
    {
        return (hints & kParameterIsBoolean) != 0x0;
    }

    juce::String getText(const float normalizedValue, const int maximumStringLength) const override
    {
        float value = ranges.getUnnormalizedValue(normalizedValue);

        if (hints & kParameterIsBoolean)
        {
            const float midRange = ranges.min + (ranges.max - ranges.min) * 0.5f;
            value = value > midRange ? ranges.max : ranges.min;
        }
        else if (hints & kParameterIsInteger)
        {
            value = std::round(value);
        }

        if (enumValues.restrictedMode)
        {
            for (uint32_t i=0; i < enumValues.count; ++i)
            {
                if (d_isEqual(enumValues.values[i].value, value))
                {
                    if (maximumStringLength <= 0)
                        return juce::String(enumValues.values[i].label);

                    return juce::String(enumValues.values[i].label, static_cast<size_t>(maximumStringLength));
                }
            }
        }

        juce::String text;
        if (hints & kParameterIsInteger)
            text = juce::String(static_cast<int>(value));
        else
            text = juce::String(value);

        if (maximumStringLength <= 0)
            return text;

        return juce::String(text.toRawUTF8(), static_cast<size_t>(maximumStringLength));
    }

    float getValueForText(const juce::String& text) const override
    {
        if (enumValues.restrictedMode)
        {
            for (uint32_t i=0; i < enumValues.count; ++i)
            {
                if (text == enumValues.values[i].label.buffer())
                    return ranges.getNormalizedValue(enumValues.values[i].value);
            }
        }

        float value;
        if (hints & kParameterIsInteger)
            value = std::atoi(text.toRawUTF8());
        else
            value = std::atof(text.toRawUTF8());

        return ranges.getFixedAndNormalizedValue(value);
    }

    bool isAutomatable() const override
    {
        return (hints & kParameterIsAutomatable) != 0x0;
    }

    juce::String getCurrentValueAsText() const override
    {
        const float value = plugin.getParameterValue(index);

        if (enumValues.restrictedMode)
        {
            for (uint32_t i=0; i < enumValues.count; ++i)
            {
                if (d_isEqual(enumValues.values[i].value, value))
                    return juce::String(enumValues.values[i].label);
            }
        }

        if (hints & kParameterIsInteger)
            return juce::String(static_cast<int>(value));

        return juce::String(value);
    }

    juce::StringArray getAllValueStrings() const override
    {
        if (dpfValueStrings.size() != 0)
            return dpfValueStrings;

        if (enumValues.restrictedMode)
        {
            for (uint32_t i=0; i < enumValues.count; ++i)
                dpfValueStrings.add(enumValues.values[i].label.buffer());

            return dpfValueStrings;
        }

        if (hints & kParameterIsBoolean)
        {
            if (hints & kParameterIsInteger)
            {
                dpfValueStrings.add(juce::String(static_cast<int>(ranges.min)));
                dpfValueStrings.add(juce::String(static_cast<int>(ranges.max)));
            }
            else
            {
                dpfValueStrings.add(juce::String(ranges.min));
                dpfValueStrings.add(juce::String(ranges.max));
            }
        }
        else if (hints & kParameterIsInteger)
        {
            const int imin = static_cast<int>(ranges.min);
            const int imax = static_cast<int>(ranges.max);

            for (int i=imin; i<=imax; ++i)
                dpfValueStrings.add(juce::String(i));
        }

        return dpfValueStrings;
    }
};

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

// unused in cardinal
static constexpr const requestParameterValueChangeFunc nullRequestParameterValueChangeFunc = nullptr;

// only needed for headless builds, which this wrapper never builds for
static constexpr const updateStateValueFunc nullUpdateStateValueFunc = nullptr;

// DSP/processor implementation
class CardinalWrapperProcessor : public juce::AudioProcessor
{
    friend class CardinalWrapperEditor;

    PluginExporter plugin;
    MidiEvent midiEvents[kMaxMidiEvents];
    TimePosition timePosition;
    const uint32_t parameterCount;

    juce::AudioProcessorParameter* bypassParameter;
    juce::MidiBuffer* currentMidiMessages;
    bool* updatedParameters;

public:
    CardinalWrapperProcessor()
        : plugin(this, writeMidiFunc, nullRequestParameterValueChangeFunc, nullUpdateStateValueFunc),
          parameterCount(plugin.getParameterCount()),
          bypassParameter(nullptr),
          currentMidiMessages(nullptr),
          updatedParameters(nullptr)
    {
        if (const double sampleRate = getSampleRate())
            if (sampleRate > 0.0)
                plugin.setSampleRate(sampleRate);

        if (const int samplesPerBlock = getBlockSize())
            if (samplesPerBlock > 0)
                plugin.setBufferSize(static_cast<uint32_t>(samplesPerBlock));

        if (parameterCount != 0)
        {
            updatedParameters = new bool[parameterCount];
            std::memset(updatedParameters, 0, sizeof(bool)*parameterCount);

            for (uint i=0; i<parameterCount; ++i)
            {
                ParameterFromDPF* const param = new ParameterFromDPF(plugin, i, updatedParameters + i);
                addParameter(param);

                if (plugin.getParameterDesignation(i) == kParameterDesignationBypass)
                    bypassParameter = param;
            }
        }
    }

    ~CardinalWrapperProcessor() override
    {
        delete[] updatedParameters;
    }

protected:
    const juce::String getName() const override
    {
        return plugin.getName();
    }

    juce::StringArray getAlternateDisplayNames() const override
    {
        return juce::StringArray(plugin.getLabel());
    }

    void prepareToPlay(const double sampleRate, const int samplesPerBlock) override
    {
        DISTRHO_SAFE_ASSERT_RETURN(samplesPerBlock > 0,);

        plugin.deactivateIfNeeded();
        plugin.setSampleRate(sampleRate);
        plugin.setBufferSize(static_cast<uint32_t>(samplesPerBlock));
        plugin.activate();
    }

    void releaseResources() override
    {
        plugin.deactivateIfNeeded();
    }

    void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) override
    {
        const int numSamples = buffer.getNumSamples();
        DISTRHO_SAFE_ASSERT_INT_RETURN(numSamples > 0, numSamples, midiMessages.clear());

        uint32_t midiEventCount = 0;

        for (const juce::MidiMessageMetadata midiMessage : midiMessages)
        {
            DISTRHO_SAFE_ASSERT_CONTINUE(midiMessage.numBytes > 0);
            DISTRHO_SAFE_ASSERT_CONTINUE(midiMessage.samplePosition >= 0);

            if (midiMessage.numBytes > static_cast<int>(MidiEvent::kDataSize))
                continue;

            MidiEvent& midiEvent(midiEvents[midiEventCount++]);

            midiEvent.frame = static_cast<uint32_t>(midiMessage.samplePosition);
            midiEvent.size = (static_cast<uint8_t>(midiMessage.numBytes));
            std::memcpy(midiEvent.data, midiMessage.data, midiEvent.size);

            if (midiEventCount == kMaxMidiEvents)
                break;
        }

        midiMessages.clear();

        const juce::ScopedValueSetter<juce::MidiBuffer*> cvs(currentMidiMessages, &midiMessages, nullptr);

        juce::AudioPlayHead* const playhead = getPlayHead();
        juce::AudioPlayHead::CurrentPositionInfo posInfo;

        if (playhead != nullptr && playhead->getCurrentPosition(posInfo))
        {
            timePosition.playing   = posInfo.isPlaying;
            timePosition.bbt.valid = true;

            // ticksPerBeat is not possible with JUCE
            timePosition.bbt.ticksPerBeat = 1920.0;

            if (posInfo.timeInSamples >= 0)
                timePosition.frame = static_cast<uint64_t>(posInfo.timeInSamples);
            else
                timePosition.frame = 0;

            timePosition.bbt.beatsPerMinute = posInfo.bpm;

            const double ppqPos    = std::abs(posInfo.ppqPosition);
            const int    ppqPerBar = posInfo.timeSigNumerator * 4 / posInfo.timeSigDenominator;
            const double barBeats  = (std::fmod(ppqPos, ppqPerBar) / ppqPerBar) * posInfo.timeSigNumerator;
            const double rest      =  std::fmod(barBeats, 1.0);

            timePosition.bbt.bar         = static_cast<int32_t>(ppqPos) / ppqPerBar + 1;
            timePosition.bbt.beat        = static_cast<int32_t>(barBeats - rest + 0.5) + 1;
            timePosition.bbt.tick        = rest * timePosition.bbt.ticksPerBeat;
            timePosition.bbt.beatsPerBar = posInfo.timeSigNumerator;
            timePosition.bbt.beatType    = posInfo.timeSigDenominator;

            if (posInfo.ppqPosition < 0.0)
            {
                --timePosition.bbt.bar;
                timePosition.bbt.beat = posInfo.timeSigNumerator - timePosition.bbt.beat + 1;
                timePosition.bbt.tick = timePosition.bbt.ticksPerBeat - timePosition.bbt.tick - 1;
            }

            timePosition.bbt.barStartTick = timePosition.bbt.ticksPerBeat*
                                            timePosition.bbt.beatsPerBar*
                                            (timePosition.bbt.bar-1);
        }
        else
        {
            timePosition.frame     = 0;
            timePosition.playing   = false;
            timePosition.bbt.valid = false;
        }

        plugin.setTimePosition(timePosition);

        DISTRHO_SAFE_ASSERT_RETURN(buffer.getNumChannels() == 2,);

        const float* audioBufferIn[2];
        float* audioBufferOut[2];
        audioBufferIn[0] = buffer.getReadPointer(0);
        audioBufferIn[1] = buffer.getReadPointer(1);
        audioBufferOut[0] = buffer.getWritePointer(0);
        audioBufferOut[1] = buffer.getWritePointer(1);

        plugin.run(audioBufferIn, audioBufferOut, static_cast<uint32_t>(numSamples), midiEvents, midiEventCount);
    }

    // fix compiler warning
    void processBlock(juce::AudioBuffer<double>&, juce::MidiBuffer&) override {}

    double getTailLengthSeconds() const override
    {
        return 0.0;
    }

    bool acceptsMidi() const override
    {
        return true;
    }

    bool producesMidi() const override
    {
        return true;
    }

    juce::AudioProcessorParameter* getBypassParameter() const override
    {
        return bypassParameter;
    }

    juce::AudioProcessorEditor* createEditor() override;

    bool hasEditor() const override
    {
        return true;
    }

    int getNumPrograms() override
    {
        return 1;
    }

    int getCurrentProgram() override
    {
        return 0;
    }

    void setCurrentProgram(int) override
    {
    }

    const juce::String getProgramName(int) override
    {
        return "Default";
    }

    void changeProgramName(int, const juce::String&) override
    {
    }

    void getStateInformation(juce::MemoryBlock& destData) override
    {
        juce::XmlElement xmlState("CardinalState");

        for (uint32_t i=0; i<parameterCount; ++i)
            xmlState.setAttribute(plugin.getParameterSymbol(i).buffer(), plugin.getParameterValue(i));

        for (uint32_t i=0, stateCount=plugin.getStateCount(); i<stateCount; ++i)
        {
            const String& key(plugin.getStateKey(i));
            xmlState.setAttribute(key.buffer(), plugin.getStateValue(key).buffer());
        }

        copyXmlToBinary(xmlState, destData);
    }

    void setStateInformation(const void* const data, const int sizeInBytes) override
    {
        std::unique_ptr<juce::XmlElement> xmlState(getXmlFromBinary(data, sizeInBytes));
        DISTRHO_SAFE_ASSERT_RETURN(xmlState.get() != nullptr,);

        const juce::Array<juce::AudioProcessorParameter*>& parameters(getParameters());

        for (uint32_t i=0; i<parameterCount; ++i)
        {
            const double value = xmlState->getDoubleAttribute(plugin.getParameterSymbol(i).buffer(),
                                                              plugin.getParameterDefault(i));
            const float normalizedValue = plugin.getParameterRanges(i).getFixedAndNormalizedValue(value);
            parameters.getUnchecked(static_cast<int>(i))->setValueNotifyingHost(normalizedValue);
        }

        for (uint32_t i=0, stateCount=plugin.getStateCount(); i<stateCount; ++i)
        {
            const String& key(plugin.getStateKey(i));
            const juce::String value = xmlState->getStringAttribute(key.buffer(),
                                                                    plugin.getStateDefaultValue(i).buffer());
            plugin.setState(key, value.toRawUTF8());
        }
    }

private:
    static bool writeMidiFunc(void* const ptr, const MidiEvent& midiEvent)
    {
        CardinalWrapperProcessor* const processor = static_cast<CardinalWrapperProcessor*>(ptr);
        DISTRHO_SAFE_ASSERT_RETURN(processor != nullptr, false);

        juce::MidiBuffer* const currentMidiMessages = processor->currentMidiMessages;
        DISTRHO_SAFE_ASSERT_RETURN(currentMidiMessages != nullptr, false);

        const uint8_t* const data = midiEvent.size > MidiEvent::kDataSize ? midiEvent.dataExt : midiEvent.data;
        return currentMidiMessages->addEvent(data,
                                             static_cast<int>(midiEvent.size),
                                             static_cast<int>(midiEvent.frame));
    }
};

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

// unused in cardinal
static constexpr const sendNoteFunc nullSendNoteFunc = nullptr;

// unwanted, juce file dialogs are ugly
static constexpr const fileRequestFunc nullFileRequestFunc = nullptr;

// UI/editor implementation
class CardinalWrapperEditor : public juce::AudioProcessorEditor,
                              private juce::Timer
{
    CardinalWrapperProcessor& cardinalProcessor;

    UIExporter* ui;
    void* const dspPtr;

public:
    CardinalWrapperEditor(CardinalWrapperProcessor& cardinalProc)
        : juce::AudioProcessorEditor(cardinalProc),
          cardinalProcessor(cardinalProc),
          ui(nullptr),
          dspPtr(cardinalProc.plugin.getInstancePointer())
    {
        setOpaque(true);
        setResizable(true, false);
        // setResizeLimits(648, 538, -1, -1);
        setSize(1228, 666);

        startTimer(1000.0 / 60.0);
    }

    ~CardinalWrapperEditor() override
    {
        stopTimer();
        delete ui;
    }

protected:
    void timerCallback() override
    {
        if (ui == nullptr)
            return;

        for (uint32_t i=0; i<cardinalProcessor.parameterCount; ++i)
        {
            if (cardinalProcessor.updatedParameters[i])
            {
                cardinalProcessor.updatedParameters[i] = false;
                ui->parameterChanged(i, cardinalProcessor.plugin.getParameterValue(i));
            }
        }

        repaint();
    }

    void paint(juce::Graphics&) override
    {
        if (ui == nullptr)
        {
            juce::ComponentPeer* const peer = getPeer();
            DISTRHO_SAFE_ASSERT_RETURN(peer != nullptr,);

            void* const nativeHandle = peer->getNativeHandle();
            DISTRHO_SAFE_ASSERT_RETURN(nativeHandle != nullptr,);

            ui = new UIExporter(this,
                (uintptr_t)nativeHandle,
                cardinalProcessor.getSampleRate(),
                editParamFunc,
                setParamFunc,
                setStateFunc,
                nullSendNoteFunc,
                setSizeFunc,
                nullFileRequestFunc,
                nullptr, // bundlePath
                dspPtr,
                0.0 // scaleFactor
            );

            if (cardinalProcessor.wrapperType == juce::AudioProcessor::wrapperType_Standalone)
            {
                const double scaleFactor = ui->getScaleFactor();
                ui->setWindowOffset(4 * scaleFactor, 30 * scaleFactor);
            }
        }

        ui->plugin_idle();
    }

private:
    static void editParamFunc(void* const ptr, const uint32_t index, const bool started)
    {
        CardinalWrapperEditor* const editor = static_cast<CardinalWrapperEditor*>(ptr);
        DISTRHO_SAFE_ASSERT_RETURN(editor != nullptr,);

        CardinalWrapperProcessor& cardinalProcessor(editor->cardinalProcessor);

        if (started)
            cardinalProcessor.getParameters().getUnchecked(static_cast<int>(index))->beginChangeGesture();
        else
            cardinalProcessor.getParameters().getUnchecked(static_cast<int>(index))->endChangeGesture();
    }

    static void setParamFunc(void* const ptr, const uint32_t index, const float value)
    {
        CardinalWrapperEditor* const editor = static_cast<CardinalWrapperEditor*>(ptr);
        DISTRHO_SAFE_ASSERT_RETURN(editor != nullptr,);

        CardinalWrapperProcessor& cardinalProcessor(editor->cardinalProcessor);
        const juce::Array<juce::AudioProcessorParameter*>& parameters(cardinalProcessor.getParameters());
        juce::AudioProcessorParameter* const parameter = parameters.getUnchecked(static_cast<int>(index));
        static_cast<ParameterFromDPF*>(parameter)->setValueNotifyingHostFromDPF(value);
    }

    static void setStateFunc(void* const ptr, const char* const key, const char* const value)
    {
        CardinalWrapperEditor* const editor = static_cast<CardinalWrapperEditor*>(ptr);
        DISTRHO_SAFE_ASSERT_RETURN(editor != nullptr,);

        CardinalWrapperProcessor& cardinalProcessor(editor->cardinalProcessor);
        cardinalProcessor.plugin.setState(key, value);
    }

    static void setSizeFunc(void* const ptr, uint width, uint height)
    {
        CardinalWrapperEditor* const editor = static_cast<CardinalWrapperEditor*>(ptr);
        DISTRHO_SAFE_ASSERT_RETURN(editor != nullptr,);

       #ifdef DISTRHO_OS_MAC
        UIExporter* const ui = editor->ui;
        DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,);

        const double scaleFactor = ui->getScaleFactor();
        width /= scaleFactor;
        height /= scaleFactor;
       #endif

        editor->setSize(static_cast<int>(width), static_cast<int>(height));
    }
};

juce::AudioProcessorEditor* CardinalWrapperProcessor::createEditor()
{
    return new CardinalWrapperEditor(*this);
}

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

END_NAMESPACE_DISTRHO

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

juce::AudioProcessor* createPluginFilter()
{
    // set valid but dummy values
    d_nextBufferSize = 512;
    d_nextSampleRate = 48000.0;
    return new DISTRHO_NAMESPACE::CardinalWrapperProcessor;
}

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