// Copyright 2023 Kushview, LLC <info@kushview.net>
// SPDX-License-Identifier: GPL-3.0-or-later

#pragma once

#include <element/context.hpp>
#include <element/services.hpp>
#include <element/audioengine.hpp>
#include <element/parameter.hpp>

#include "ElementApp.h"

namespace element {

//=============================================================================
class PerformanceParameter : public juce::HostedAudioProcessorParameter,
                             public element::Parameter::Listener
{
public:
    std::function<void()> onCleared;

    explicit PerformanceParameter (int paramIdx)
        : juce::HostedAudioProcessorParameter (1),
          index (paramIdx)
    {
        clearNode();
    }

    ~PerformanceParameter()
    {
        clearNode();
    }

    bool haveNode() const { return node != nullptr; }

    juce::String getBoundParameterName() const
    {
        juce::SpinLock::ScopedLockType sl (lock);
        return parameter != nullptr ? parameter->getName (100) : String();
    }

    void clearNode()
    {
        ProcessorPtr oldNode;
        element::ParameterPtr oldParam;

        if (parameter)
            parameter->removeListener (this);
        removedConnection.disconnect();

        {
            SpinLock::ScopedLockType sl (lock);
            special = false;
            processor = nullptr;
            oldNode = node;
            node = nullptr;
            oldParam = parameter;
            parameter = nullptr;
            parameterIdx = Processor::NoParameter;
        }

        oldParam.reset();
        oldNode.reset();
        model = Node();

        if (onCleared)
            onCleared();
    }

    void bindToNode (const Node& newNode, int newParam)
    {
        if (newNode == model)
            return;

        model = newNode;
        ProcessorPtr newNodeObj = model.getObject();

        {
            juce::SpinLock::ScopedLockType sl (lock);
            parameterIdx = newParam;
            node = newNodeObj;
            processor = (node != nullptr) ? node->getAudioProcessor() : nullptr;
            parameter = nullptr;
            if (isPositiveAndBelow (parameterIdx, node->getParameters().size()))
                parameter = node->getParameters()[parameterIdx];
        }

        if (node)
            removedConnection = node->willBeRemoved.connect (
                std::bind (&PerformanceParameter::clearNode, this));

        if (parameter != nullptr)
            parameter->addListener (this);
    }

    void setAndNotify (float value)
    {
        if (parameter)
            parameter->setValueNotifyingHost (value);
    }

    void updateValue()
    {
        if (parameter)
        {
            setValueNotifyingHost (parameter->getValue());
        }
        else
        {
            switch (parameterIdx)
            {
                case Processor::EnabledParameter:
                    setValueNotifyingHost (node->isEnabled() ? 1.f : 0.f);
                    break;
                case Processor::BypassParameter:
                    setValueNotifyingHost (node->isSuspended() ? 1.f : 0.f);
                    break;
                case Processor::MuteParameter:
                    setValueNotifyingHost (node->isMuted() ? 1.f : 0.f);
                    break;
            }
        }
    }

    float getValue() const override
    {
        juce::SpinLock::ScopedLockType sl (lock);
        return (parameter != nullptr) ? parameter->getValue() : value.get();
    }

    void setValue (float newValue) override
    {
        value.set (newValue);
        juce::SpinLock::ScopedLockType sl (lock);

        if (parameter != nullptr)
        {
            parameter->setValue (value.get());
        }
        else
        {
        }
    }

    float getDefaultValue() const override
    {
        juce::SpinLock::ScopedLockType sl (lock);
        if (parameter != nullptr)
            return parameter->getDefaultValue();

        switch (parameterIdx)
        {
            case Processor::MuteParameter:
                return 0.f;
                break;
            case Processor::EnabledParameter:
                return 1.f;
                break;
            case Processor::BypassParameter:
                return 0.f;
                break;
        }

        return 0.f;
    }

    juce::String getParameterID() const override
    {
        return getName (32).toLowerCase().replace (" ", "-");
    }

    juce::String getName (int maximumStringLength) const override
    {
        String name ("Parameter ");
        name << int (index + 1);
        return name.substring (0, maximumStringLength);
    }

    juce::String getLabel() const override
    {
        return parameter != nullptr ? parameter->getLabel() : String();
    }

    /** Should parse a string and return the appropriate value for it. */
    float getValueForText (const juce::String& text) const override
    {
        return parameter != nullptr ? parameter->getValueForText (text)
                                    : jlimit (0.f, 1.f, text.getFloatValue());
    }

    int getNumSteps() const override
    {
        if (parameter != nullptr)
            return parameter->getNumSteps();

        switch (parameterIdx)
        {
            case Processor::MuteParameter:
            case Processor::EnabledParameter:
            case Processor::BypassParameter:
                return 1;
                break;
        }

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

    bool isDiscrete() const override
    {
        return (parameter != nullptr) ? parameter->isDiscrete()
                                      : AudioProcessorParameter::isDiscrete();
    }

    bool isBoolean() const override
    {
        if (parameter != nullptr)
            return parameter->isBoolean();

        switch (parameterIdx)
        {
            case Processor::MuteParameter:
            case Processor::EnabledParameter:
            case Processor::BypassParameter:
                return true;
                break;
        }

        return AudioProcessorParameter::isBoolean();
    }

    bool isMetaParameter() const override
    {
        return (parameter != nullptr) ? parameter->isMetaParameter()
                                      : AudioProcessorParameter::isMetaParameter();
    }

    juce::AudioProcessorParameter::Category getCategory() const override
    {
        return (parameter != nullptr)
                   ? static_cast<juce::AudioProcessorParameter::Category> (parameter->getCategory())
                   : juce::AudioProcessorParameter::getCategory();
    }

    juce::String getText (float value, int length) const override
    {
        return (parameter != nullptr)
                   ? parameter->getText (value, length)
                   : juce::AudioProcessorParameter::getText (value, length);
    }

    bool isOrientationInverted() const override
    {
        return (parameter != nullptr) ? parameter->isOrientationInverted()
                                      : juce::AudioProcessorParameter::isOrientationInverted();
    }

    //=========================================================================

    void controlValueChanged (int parameterIndex, float newValue) override
    {
        if (recursionBlock)
            return;
        ignoreUnused (parameterIndex, newValue);
        recursionBlock = true;
        updateValue();
        recursionBlock = false;
    }

    void controlTouched (int, bool touched) override
    {
        if (touched)
            beginChangeGesture();
        else
            endChangeGesture();
    }

    //=========================================================================
#if 0
    
    /** This can be overridden to tell the host that this parameter operates in the
     reverse direction.
     (Not all plugin formats or hosts will actually use this information).
     */
    virtual bool isOrientationInverted() const;
    
    /** Returns true if the host can automate this parameter.
     By default, this returns true.
     */
    virtual bool isAutomatable() const;
    
    
    //==============================================================================
    /** Returns the current value of the parameter as a String.
     
     This function can be called when you are hosting plug-ins to get a
     more specialsed textual represenation of the current value from the
     plug-in, for example "On" rather than "1.0".
     
     If you are implementing a plug-in then you should ignore this function
     and instead override getText.
     */
    virtual String getCurrentValueAsText() const;
    
    /** Returns the set of strings which represent the possible states a parameter
     can be in.
     
     If you are hosting a plug-in you can use the result of this function to
     populate a ComboBox listing the allowed values.
     
     If you are implementing a plug-in then you do not need to override this.
     */
    virtual StringArray getAllValueStrings() const;
#endif

public:
    Node getNode() const { return model; }
    int getBoundParameter() const
    {
        juce::SpinLock::ScopedLockType sl (lock);
        return parameterIdx;
    }

private:
    juce::SpinLock lock;
    const int index;
    juce::Atomic<float> value { 0.f };
    Node model;
    ProcessorPtr node;
    juce::AudioProcessor* processor = nullptr;
    element::ParameterPtr parameter = nullptr;
    int parameterIdx = -1;
    bool special = false;
    bool recursionBlock = false;
    SignalConnection removedConnection;
};

class PluginProcessor : public juce::AudioProcessor,
                        private juce::AsyncUpdater
{
public:
    enum Variant
    {
        Instrument,
        Effect,
        MidiEffect
    };

    explicit PluginProcessor (Variant instanceType = Instrument, int numBuses = 0);
    ~PluginProcessor() override;

    const String getName() const override;
    Variant getVariant() const { return variant; }

    void prepareToPlay (double sampleRate, int samplesPerBlock) override;
    void processBlock (AudioSampleBuffer&, MidiBuffer&) override;
    void releaseResources() override;
    void reset() override;

    bool isNodeBoundToAnyPerformanceParameter (const Node& boundNode, int boundParam) const;
    PopupMenu getPerformanceParameterMenu (int perfParam);
    void handlePerformanceParameterResult (int result, int perfParam);

    //==========================================================================
    AudioProcessorEditor* createEditor() override;
    bool hasEditor() const override;

    //==========================================================================
    bool acceptsMidi() const override;
    bool producesMidi() const override;
    bool isMidiEffect() const override;

    //==========================================================================
    double getTailLengthSeconds() const override;

    //==========================================================================
    int getNumPrograms() override;
    int getCurrentProgram() override;
    void setCurrentProgram (int index) override;
    const String getProgramName (int index) override;
    void changeProgramName (int index, const String& newName) override;

    //==========================================================================
    void getStateInformation (MemoryBlock& destData) override;
    void setStateInformation (const void* data, int sizeInBytes) override;

    //==========================================================================
    void numChannelsChanged() override;
    void numBusesChanged() override;
    void processorLayoutsChanged() override;

    Services* getServices() const { return context != nullptr ? &context->services() : nullptr; }

    void setEditorBounds (const Rectangle<int>& bounds) { editorBounds = bounds; }
    const Rectangle<int>& getEditorBounds() const { return editorBounds; }

    void updateLatencySamples();

    bool getEditorWantsKeyboard() const { return editorWantsKeyboard; }
    void setEditorWantsKeyboard (bool wantsIt) { editorWantsKeyboard = wantsIt; }

    void setForceZeroLatency (bool);
    bool isForcingZeroLatency() const { return forceZeroLatency; }

    Signal<void()> onPerfParamsChanged;

protected:
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override;

private:
    Array<PerformanceParameter*> perfparams;
    struct PerfParamMenuItem
    {
        Node node;
        int parameter = -1;
        bool unlink = false;
    };

    const Variant variant;

    OwnedArray<PerfParamMenuItem> menuMap;
    std::unique_ptr<Context> context;
    AudioEnginePtr engine;

    bool initialized = false;
    bool prepared = false;
    int preparedCount = 0;

    double sampleRate = 44100.0;
    int bufferSize = 512;
    int numIns = 0;
    int numOuts = 2;

    Rectangle<int> editorBounds;
    bool editorWantsKeyboard = false;

    Atomic<bool> shouldProcess = false;
    bool controllerActive = false;
    bool loadSessionOnPrepare = false;

    bool forceZeroLatency = false;

    class AsyncPrepare : public AsyncUpdater
    {
        AudioProcessor& processor;
        int bufferSize = 0;
        double sampleRate = 0.0;

    public:
        AsyncPrepare (AudioProcessor& p)
            : processor (p) {}
        ~AsyncPrepare() { cancelPendingUpdate(); }

        void prepare (double s, int b)
        {
            cancelPendingUpdate();
            bufferSize = b;
            sampleRate = s;
            triggerAsyncUpdate();
        }

        void handleAsyncUpdate() override
        {
            processor.prepareToPlay (sampleRate, bufferSize);
        }
    };

    std::unique_ptr<AsyncPrepare> asyncPrepare;

    struct Latency;
    std::unique_ptr<Latency> _latency;

    void initialize();
    friend class AsyncUpdater;
    void handleAsyncUpdate() override;
    void reloadEngine();

    int calculateLatencySamples() const;
    static BusesProperties createDefaultBuses (Variant variant, int numAux);
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginProcessor)
};

} // namespace element
