// Copyright (c) 2022 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#pragma once

#include <vector>
#include <memory>
#include "json.hpp"
#include "PluginType.hpp"
#include "PedalBoard.hpp"
#include <lilv/lilv.h>
#include "MapFeature.hpp"
#include "LogFeature.hpp"
#include "OptionsFeature.hpp"
#include <filesystem>
#include <cmath>

#include "lv2.h"
#include "Units.hpp"
#include "PluginPreset.hpp"


namespace pipedal {


// forward declarations
class Lv2Effect;
class Lv2PedalBoard;
class Lv2Host;
class JackConfiguration;
class JackChannelSelection;

#define LV2_PROPERTY_GETSET(name) \
    const decltype(name##_) & name() const { return name##_; }; \
    decltype(name##_) & name()  { return name##_; }; \
    void  name(const decltype(name##_) &value) { name##_ = value; }; 


#define LV2_PROPERTY_GETSET_SCALAR(name) \
    decltype(name##_) name() const { return name##_;}; \
    void  name(decltype(name##_) value) { name##_ = value; }; 





class Lv2PluginClass {
public:
    friend class Lv2Host;
private:
    Lv2PluginClass*  parent_ = nullptr; // NOT SERIALIZED!
    std::string parent_uri_;
    std::string display_name_;
    std::string uri_;
    PluginType plugin_type_;
    std::vector<std::shared_ptr<Lv2PluginClass>> children_;


    friend class ::pipedal::Lv2Host;
    // hide copy constructor.
    Lv2PluginClass(const Lv2PluginClass &other)
    {

    }
    void set_parent(std::shared_ptr<Lv2PluginClass> &parent)
    {
        this->parent_ = parent.get();
    }
    void add_child(std::shared_ptr<Lv2PluginClass> &child) {
        for (size_t i = 0; i < children_.size(); ++i)
        {
            if (children_[i]->uri_ == child->uri_)
            {
                return;
            }
        }
        children_.push_back(child);
    }
public:
    Lv2PluginClass(
        const char*display_name, const char*uri, const char*parent_uri)
    : parent_uri_(parent_uri)
    , display_name_(display_name)
    , uri_(uri)
    , plugin_type_(uri_to_plugin_type(uri))
    {

    }
    Lv2PluginClass() { }
    LV2_PROPERTY_GETSET(uri)
    LV2_PROPERTY_GETSET(display_name)
    LV2_PROPERTY_GETSET(parent_uri)

    const Lv2PluginClass *parent() { return parent_;}

    bool operator==(const Lv2PluginClass &other)
    {
        return uri_ == other.uri_;
    }
    bool is_a(const std::string& classUri) const;

    static json_map::storage_type<Lv2PluginClass> jmap;

};
class Lv2PluginClasses {
private:
    std::vector<std::string>  classes_;
public:
    Lv2PluginClasses()
    {

    }
    Lv2PluginClasses(std::vector<std::string>  classes)
    :classes_(classes)
    {

    }
    const std::vector<std::string>  & classes() const
    {
        return classes_;
    }
    bool is_a(Lv2Host*lv2Plugins,const char*classUri) const;

    static json_map::storage_type<Lv2PluginClasses> jmap;

};


class Lv2ScalePoint {
private:
    float value_;
    std::string label_;
public:
    Lv2ScalePoint() { }
    Lv2ScalePoint(float value, std::string label)
    : value_(value)
    , label_(label)
    {

    }

    LV2_PROPERTY_GETSET_SCALAR(value);
    LV2_PROPERTY_GETSET(label);
    
    static json_map::storage_type<Lv2ScalePoint> jmap;
};


enum class Lv2BufferType {
    None,
    Event,
    Sequence,
    Unknown
};

class Lv2PortInfo {
public:
    Lv2PortInfo(Lv2Host*lv2Host,const LilvPlugin*pPlugin,const LilvPort *pPort);
private:
    friend class Lv2PluginInfo;

    uint32_t index_;
    std::string symbol_;
    std::string name_;
    float min_value_, max_value_, default_value_;
    Lv2PluginClasses classes_;
    std::vector<Lv2ScalePoint> scale_points_;
    bool is_input_ = false;
    bool is_output_ = false;

    bool is_control_port_ = false;
    bool is_audio_port_ = false;
    bool is_atom_port_ = false;
    bool is_cv_port_ = false;

    bool is_valid_ = false;
    bool supports_midi_ = false;
    bool supports_time_position_ = false;
    bool is_logarithmic_ = false;
    int display_priority_ = -1;
    int range_steps_ = 0;
    bool trigger_;
    bool integer_property_;
    bool enumeration_property_;
    bool toggled_property_;
    bool not_on_gui_;
    std::string buffer_type_;
    std::string port_group_;

    std::string designation_;
    Units units_ = Units::none;
public:
    bool IsSwitch() const {
        return min_value_ == 0 && max_value_ == 1 
            && (integer_property_ || toggled_property_ || enumeration_property_);
    }
    float rangeToValue(float range) const
    {
        float value;
        if (is_logarithmic_)
        {
            value = std::pow(max_value_/min_value_,range)*min_value_;
        } else {
            value = (max_value_-min_value_)*range+min_value_;
        }
        if (integer_property_ || enumeration_property_)
        {
            value = std::round(value);
        } else if (range_steps_ >= 2)
        {
            value = std::round(value*(range_steps_-1))/(range_steps_-1);
        }
        if (toggled_property_)
        {
            value = value == 0 ? 0: max_value_;
        }
        if (value > max_value_) value = max_value_;
        if (value < min_value_) value = min_value_;
        return value;
    }
    LV2_PROPERTY_GETSET(symbol);
    LV2_PROPERTY_GETSET_SCALAR(index);
    LV2_PROPERTY_GETSET(name);
    LV2_PROPERTY_GETSET(classes);
    LV2_PROPERTY_GETSET(scale_points);
    LV2_PROPERTY_GETSET_SCALAR(min_value);
    LV2_PROPERTY_GETSET_SCALAR(max_value);
    LV2_PROPERTY_GETSET_SCALAR(default_value);

    LV2_PROPERTY_GETSET_SCALAR(is_input);
    LV2_PROPERTY_GETSET_SCALAR(is_output);
    LV2_PROPERTY_GETSET_SCALAR(is_control_port);
    LV2_PROPERTY_GETSET_SCALAR(is_audio_port);
    LV2_PROPERTY_GETSET_SCALAR(is_atom_port);
    LV2_PROPERTY_GETSET_SCALAR(is_cv_port);
    LV2_PROPERTY_GETSET_SCALAR(is_valid);
    LV2_PROPERTY_GETSET_SCALAR(supports_midi);
    LV2_PROPERTY_GETSET_SCALAR(supports_time_position);
    LV2_PROPERTY_GETSET_SCALAR(is_logarithmic);
    LV2_PROPERTY_GETSET_SCALAR(display_priority);
    LV2_PROPERTY_GETSET_SCALAR(range_steps);
    LV2_PROPERTY_GETSET_SCALAR(trigger);
    LV2_PROPERTY_GETSET_SCALAR(integer_property);
    LV2_PROPERTY_GETSET_SCALAR(enumeration_property);
    LV2_PROPERTY_GETSET_SCALAR(toggled_property);
    LV2_PROPERTY_GETSET_SCALAR(not_on_gui);
    LV2_PROPERTY_GETSET(port_group);
    LV2_PROPERTY_GETSET_SCALAR(units);

    LV2_PROPERTY_GETSET(buffer_type);

    Lv2BufferType GetBufferType() {
        if (buffer_type_ == "") return Lv2BufferType::None;
        if (buffer_type_ == LV2_ATOM__Sequence) return Lv2BufferType::Sequence;
        return Lv2BufferType::Unknown;
    }

public:
    Lv2PortInfo() { }
    ~Lv2PortInfo() = default;
    bool is_a(Lv2Host*lv2Plugins,const char*classUri);

    static json_map::storage_type<Lv2PortInfo> jmap;
    
};

class Lv2PortGroup {
private:
    std::string uri_;
    std::string symbol_;
    std::string name_;

public:
    LV2_PROPERTY_GETSET(uri);
    LV2_PROPERTY_GETSET(symbol);
    LV2_PROPERTY_GETSET(name);

    Lv2PortGroup() { }
    Lv2PortGroup(Lv2Host*lv2Host,const std::string &groupUri);

    static json_map::storage_type<Lv2PortGroup> jmap;
};

class Lv2PluginInfo {
private:
    friend class Lv2Host;
public:
    Lv2PluginInfo(Lv2Host*lv2Host,const LilvPlugin*);
    Lv2PluginInfo() { }
private:
    bool HasFactoryPresets(Lv2Host*lv2Host, const LilvPlugin*plugin);
    std::string uri_;
    std::string name_;
    std::string plugin_class_;
    std::vector<std::string> supported_features_;
    std::vector<std::string> required_features_;
    std::vector<std::string> optional_features_;
    std::vector<std::string> extensions_;
    bool has_factory_presets_ = false;

    std::string author_name_;
    std::string author_homepage_;

    std::string comment_;
    std::vector<std::shared_ptr<Lv2PortInfo> > ports_;
    std::vector<std::shared_ptr<Lv2PortGroup> > port_groups_;
    bool is_valid_ = false;

    bool IsSupportedFeature(const std::string &feature) const;

public:
    LV2_PROPERTY_GETSET(uri)
    LV2_PROPERTY_GETSET(name)
    LV2_PROPERTY_GETSET(plugin_class)
    LV2_PROPERTY_GETSET(supported_features)
    LV2_PROPERTY_GETSET(required_features)
    LV2_PROPERTY_GETSET(optional_features)
    LV2_PROPERTY_GETSET(author_name)
    LV2_PROPERTY_GETSET(author_homepage)
    LV2_PROPERTY_GETSET(comment)
    LV2_PROPERTY_GETSET(extensions)
    LV2_PROPERTY_GETSET(ports)
    LV2_PROPERTY_GETSET(is_valid)
    LV2_PROPERTY_GETSET(port_groups)
    LV2_PROPERTY_GETSET(has_factory_presets)

    const Lv2PortInfo& getPort(const std::string&symbol)
    {
        for (size_t i = 0; i < ports_.size(); ++i)
        {
            if (ports_[i]->symbol() == symbol)
            {
                return *(ports_[i].get());
            }
        }
        throw PiPedalArgumentException("Port not found.");
    }
    bool hasExtension(const std::string&uri)
    {
        for (int i = 0; i < extensions_.size(); ++i)
        {
            if (extensions_[i] == uri) return true;
        }
        return false;
    }
    bool hasCvPorts() const {
        for (size_t i = 0; i < ports_.size(); ++i)
        {
            if (ports_[i]->is_cv_port())
            {
                return true;
            }
        }
        return false;
    }
    bool hasMidiInput() const {
        for (size_t i = 0; i < ports_.size(); ++i)
        {
            if (ports_[i]->is_atom_port() && ports_[i]->supports_midi() && ports_[i]->is_input())
            {
                return true;
            }
        }
    }
    bool hasMidiOutput() const {
        for (size_t i = 0; i < ports_.size(); ++i)
        {
            if (ports_[i]->is_atom_port() && ports_[i]->supports_midi() && ports_[i]->is_output())
            {
                return true;
            }
        }
    }


public:

    virtual ~Lv2PluginInfo();

    static json_map::storage_type<Lv2PluginInfo> jmap;

};


class Lv2PluginUiControlPort {
public:
    Lv2PluginUiControlPort() {
    }
    Lv2PluginUiControlPort(const Lv2PluginInfo *pPlugin,const Lv2PortInfo*pPort)
    : symbol_(pPort->symbol())
    , index_(pPort->index())
    , name_(pPort->name())
    , min_value_(pPort->min_value())
    , max_value_(pPort->max_value())
    , default_value_(pPort->default_value())
    , range_steps_(pPort->range_steps())
    , display_priority_(pPort->display_priority())
    , is_logarithmic_(pPort->is_logarithmic())
    , integer_property_(pPort->integer_property())
    , enumeration_property_(pPort->enumeration_property())
    , toggled_property_(pPort->toggled_property())
    , not_on_gui_(pPort->not_on_gui())
    , scale_points_(pPort->scale_points())
    , units_(pPort->units())
    {
        // Use symbols to index port groups, instead of uris.
        // symbols are guaranteed to be unique.
        auto &portGroup = pPort->port_group();
        for (int i = 0; i < pPlugin->port_groups().size(); ++i)
        {

            auto &p = pPlugin->port_groups()[i];
            if (p->uri() == portGroup)
            {
                this->port_group_ = p->symbol();
                break;
            }
            

        }   
    }
private:
    std::string symbol_;
    int index_;
    std::string name_;
    float min_value_ = 0, max_value_ = 1,default_value_ = 0;
    int range_steps_ = 0;
    int display_priority_ = -1;
    bool is_logarithmic_ = false;
    bool integer_property_ = false;
    bool enumeration_property_ = false;
    bool toggled_property_ = false;
    bool not_on_gui_ = false;
    std::vector<Lv2ScalePoint> scale_points_;
    std::string port_group_;
    Units units_ = Units::none;
public:
    LV2_PROPERTY_GETSET(symbol);
    LV2_PROPERTY_GETSET_SCALAR(index);
    LV2_PROPERTY_GETSET(name);
    LV2_PROPERTY_GETSET_SCALAR(min_value);
    LV2_PROPERTY_GETSET_SCALAR(max_value);
    LV2_PROPERTY_GETSET_SCALAR(default_value);
    LV2_PROPERTY_GETSET_SCALAR(range_steps);
    LV2_PROPERTY_GETSET_SCALAR(display_priority);
    LV2_PROPERTY_GETSET_SCALAR(is_logarithmic);
    LV2_PROPERTY_GETSET_SCALAR(integer_property);
    LV2_PROPERTY_GETSET_SCALAR(enumeration_property);
    LV2_PROPERTY_GETSET_SCALAR(toggled_property);
    LV2_PROPERTY_GETSET_SCALAR(not_on_gui);
    LV2_PROPERTY_GETSET(scale_points);
    LV2_PROPERTY_GETSET(units);

public:
    static json_map::storage_type<Lv2PluginUiControlPort> jmap;

};

class Lv2PluginUiPortGroup {
private:
    std::string symbol_;
    std::string name_;
public:
    Lv2PluginUiPortGroup() { }
    Lv2PluginUiPortGroup(Lv2PortGroup *pPortGroup)
    : symbol_(pPortGroup->symbol())
    , name_(pPortGroup->name())
    {
    }
public:
    static json_map::storage_type<Lv2PluginUiPortGroup> jmap;

};

class Lv2PluginUiInfo {
public:
    Lv2PluginUiInfo() { }
    Lv2PluginUiInfo(Lv2Host *pPlugins,const Lv2PluginInfo *plugin);

private:
    std::string uri_;
    std::string name_;
    std::string author_name_;
    std::string author_homepage_;
    PluginType plugin_type_;
    std::string plugin_display_type_;
    int audio_inputs_ = 0;
    int audio_outputs_ = 0;
    int has_midi_input_ = false;
    int has_midi_output_ = false;
    std::string description_;

    std::vector<Lv2PluginUiControlPort> controls_;
    std::vector<Lv2PluginUiPortGroup> port_groups_;

public:
    LV2_PROPERTY_GETSET(uri)
    LV2_PROPERTY_GETSET(name)
    LV2_PROPERTY_GETSET(author_name)
    LV2_PROPERTY_GETSET(author_homepage)
    LV2_PROPERTY_GETSET_SCALAR(plugin_type)
    LV2_PROPERTY_GETSET(plugin_display_type)
    LV2_PROPERTY_GETSET_SCALAR(audio_inputs)
    LV2_PROPERTY_GETSET_SCALAR(audio_outputs)
    LV2_PROPERTY_GETSET_SCALAR(has_midi_input)
    LV2_PROPERTY_GETSET_SCALAR(has_midi_output)
    LV2_PROPERTY_GETSET_SCALAR(description)
    LV2_PROPERTY_GETSET(port_groups)


    static json_map::storage_type<Lv2PluginUiInfo> jmap;

};

class IHost {
public:
    virtual LilvWorld*getWorld() = 0;
    virtual LV2_URID_Map* GetLv2UridMap()  = 0;
    virtual LV2_URID GetLv2Urid(const char*uri) = 0;
    virtual std::string Lv2UriudToString(LV2_URID urid) = 0;
    
    virtual LV2_Feature*const*GetLv2Features() const = 0;
    virtual double GetSampleRate() const = 0;
    virtual void SetMaxAudioBufferSize(size_t size) = 0;
    virtual size_t GetMaxAudioBufferSize() const = 0;
    virtual size_t GetAtomBufferSize() const = 0;

    virtual bool HasMidiInputChannel() const = 0;
    virtual int GetNumberOfInputAudioChannels() const = 0;
    virtual int GetNumberOfOutputAudioChannels() const = 0;
    virtual std::shared_ptr<Lv2PluginInfo> GetPluginInfo(const std::string&uri) const = 0;


    virtual Lv2Effect*CreateEffect(const PedalBoardItem &pedalBoard) = 0;
};


class LilvNodePtr {
protected:
    LilvNode *node = nullptr;
    LilvNodePtr(const LilvNodePtr&) {}; // no copy.
    LilvNodePtr(const LilvNode*) {}; // const LilvNodes are owned by lilv by convention.
public:
    LilvNodePtr() { }
    LilvNodePtr(LilvNode*node) { this->node = node; }
    ~LilvNodePtr() { Free(); }
    void Free() { if (node != nullptr) lilv_node_free(node); node = nullptr;}

    operator const LilvNode *()
    {
        return this->node;
    }

    LilvNode **operator&()
    {
        return &(this->node);
    }
    operator bool()
    {
        return this->node != nullptr;
    }
    LilvNodePtr&operator=(LilvNode*node) { Free(); this->node = node; return *this;}
};

class Lv2Host: private IHost {
private:
    static const char *RDFS_COMMENT_URI;
    class LilvUris {
    public:
        void Initialize(LilvWorld*pWorld);
        void Free();

        LilvNodePtr  rdfsComment;
        LilvNodePtr logarithic_uri;
        LilvNodePtr display_priority_uri;
        LilvNodePtr range_steps_uri;
        LilvNodePtr integer_property_uri;
        LilvNodePtr enumeration_property_uri;
        LilvNodePtr toggle_property_uri;
        LilvNodePtr not_on_gui_property_uri;
        LilvNodePtr midiEventNode;
        LilvNodePtr designationNode;
        LilvNodePtr portGroupUri;
        LilvNodePtr unitsUri;
        LilvNodePtr bufferType_uri;
        LilvNodePtr pset_Preset;
        LilvNodePtr rdfs_label;
        LilvNodePtr symbolUri;
        LilvNodePtr nameUri;

        LilvNodePtr time_Position;
        LilvNodePtr time_barBeat;
        LilvNodePtr time_beatsPerMinute;
        LilvNodePtr time_speed;

        LilvNodePtr appliesTo;
        LilvNodePtr isA;


    };
    LilvUris lilvUris;

    LilvNode *get_comment(const std::string &uri);


    size_t maxBufferSize = 1024;
    size_t maxAtomBufferSize = 16*1024;
    bool hasMidiInputChannel;
    int numberOfAudioInputChannels = 2;
    int numberOfAudioOutputChannels = 2;
    double sampleRate = 0;

    LV2_Feature*const*lv2Features = nullptr;
    MapFeature mapFeature;
    LogFeature logFeature;
    OptionsFeature optionsFeature;

    static void fn_LilvSetPortValueFunc(const char* port_symbol,
                                     void*       user_data,
                                     const void* value,
                                     uint32_t    size,
                                     uint32_t    type);

    LilvWorld *pWorld;
    void free_world();


    std::vector<std::shared_ptr<Lv2PluginInfo> > plugins_;
    std::vector<Lv2PluginUiInfo> ui_plugins_;

    std::map<std::string,std::shared_ptr<Lv2PluginClass> > classesMap;

    friend class Lv2PluginInfo;
    friend class Lv2PortInfo;
    friend class Lv2PortGroup;

    std::shared_ptr<Lv2PluginClass> GetPluginClass(const LilvPluginClass*pClass);
    std::shared_ptr<Lv2PluginClass> MakePluginClass(const LilvPluginClass*pClass);
    Lv2PluginClasses GetPluginPortClass(const LilvPlugin*lilvPlugin,const LilvPort *lilvPort);

    bool classesLoaded = false;

private:
    // IHost implementation 
    virtual LilvWorld*getWorld() {
        return pWorld;
    }
    virtual void SetMaxAudioBufferSize(size_t size) { maxBufferSize = size; }
    virtual size_t GetMaxAudioBufferSize() const { return maxBufferSize; }
    virtual size_t GetAtomBufferSize() const { return maxAtomBufferSize; }
    virtual bool HasMidiInputChannel() const { return hasMidiInputChannel;}
    virtual int GetNumberOfInputAudioChannels() const { return numberOfAudioInputChannels; }
    virtual int GetNumberOfOutputAudioChannels() const { return numberOfAudioOutputChannels;}
    virtual LV2_Feature*const* GetLv2Features() const { return this->lv2Features; }
    virtual LV2_URID_Map* GetLv2UridMap()  {
        return this->mapFeature.GetMap();
    }
    static void PortValueCallback(const char*symbol,void*user_data,const void* value,uint32_t size, uint32_t type);

    virtual Lv2Effect*CreateEffect(const PedalBoardItem &pedalBoardItem);
    void LoadPluginClassesFromLilv();
    void AddJsonClassesToMap(std::shared_ptr<Lv2PluginClass> pluginClass);

public:
    Lv2Host();
    virtual ~Lv2Host();

    IHost* asIHost() { return this;}

    virtual Lv2PedalBoard * CreateLv2PedalBoard(const PedalBoard& pedalBoard);

    void setSampleRate(double sampleRate)
    {
        this->sampleRate = sampleRate;
    }
    double GetSampleRate() const {
        return sampleRate;
    }

    class Urids;

    Urids *urids;

    void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings);

    std::shared_ptr<Lv2PluginClass> GetPluginClass(const std::string&uri) const;
    bool is_a(const std::string&class_, const std::string & target_class);

    std::shared_ptr<Lv2PluginClass> GetLv2PluginClass() const;

    std::vector<std::shared_ptr<Lv2PluginInfo> > GetPlugins() const { return plugins_; }
    const std::vector<Lv2PluginUiInfo> &GetUiPlugins() const { return ui_plugins_; }

    virtual std::shared_ptr<Lv2PluginInfo> GetPluginInfo(const std::string&uri) const;


    static constexpr const char * DEFAULT_LV2_PATH="/usr/lib/lv2";


    void LoadPluginClassesFromJson(std::filesystem::path jsonFile);
    void Load(const char*lv2Path = Lv2Host::DEFAULT_LV2_PATH);

    virtual LV2_URID GetLv2Urid(const char*uri)  {
        return this->mapFeature.GetUrid(uri);
    }
    virtual std::string Lv2UriudToString(LV2_URID urid) {
        return this->mapFeature.UridToString(urid);
    }

    PluginPresets GetFactoryPluginPresets(const std::string &pluginUri);
    std::vector<ControlValue>  LoadFactoryPluginPreset(PedalBoardItem*pedalBoardItem,
        const std::string&presetUri);


};

#undef LV2_PROPERTY_GETSET

} // namespace pipedal.