//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Descriptor/SelectionProperty.h
//! @brief     Defines class SelectionProperty
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2021
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#ifndef BORNAGAIN_GUI_MODEL_DESCRIPTOR_SELECTIONPROPERTY_H
#define BORNAGAIN_GUI_MODEL_DESCRIPTOR_SELECTIONPROPERTY_H

#include "GUI/Support/XML/UtilXML.h"

//! Abstract base class for SelectionProperty to support untemplated references.
class ISelectionProperty {
public:
    virtual ~ISelectionProperty() = default;

    //! Set currently selected option
    virtual void setCurrentIndex(int newIndex) = 0;

    //! Get currently selected option
    virtual int currentIndex() const = 0;
};

//! Class for representing a selection, its attributes and its accessors.
//!
//! A "selection" in this context is a class instance out of a range of possible class instances.
//!
//! Example:
//! A distribution can be represented by DistributionGateItem, DistributionLorentzItem and more. The
//! common base class is DistributionItem. To store the currently distribution, you may use a member
//! of DistributionItem* which holds an instance of the currently selected distribution (e.g.
//! DistributionGateItem*). To select between distributions, you need a combo box filled with the
//! distribution names, a label for the combo box, and so on.
//!
//! This class provides many of these attributes and functionalities:
//! * A pointer to the common base class (given as the template argument T). The lifetime of this
//! pointer is handled in here.
//! * label: a label of e.g. a spin box
//! * tooltip: tooltip for e.g. a spin box
//! * a list of available options (e.g. the names of the distributions)
//! * setters and getters
//!
//! The initialization of a SelectionProperty is done using a catalog.
//!
template <typename Catalog>
class SelectionProperty : public ISelectionProperty {
public:
    using CatalogedType = typename Catalog::CatalogedType;

    //! Initialize by means of a catalog class and optional creation arguments.
    //!
    //! The current selection will be initialized with the first type in the catalog types. The
    //! optional arguments are the arguments which may be necessary for the creation method in the
    //! catalog.
    template <typename... ArgsForCreation>
    void init(const QString& label, const QString& tooltip, ArgsForCreation... argsForCreation)
    {
        initFieldsAndSetter(label, tooltip, argsForCreation...);
        setCurrentIndex(0);
    }

    //! Initialize by means of a catalog class and an initializer function.
    //!
    //! The current selection will be initialized with the first type in the catalog types.
    //! Each newly created item can be initialized with the given initializer method. Note that the
    //! item creation will take place also after a call to this method, so take care that the given
    //! initializer stays valid throughout the lifetime of this SelectionProperty instance.
    //!
    //! The initializer function takes two arguments: The first is the new item, the second is the
    //! old one (if present; can be null). This is intended to maybe copy values from the old to the
    //! new selection. The old item also can be ignored, always according to the current
    //! needs.
    void initWithInitializer(
        const QString& label, const QString& tooltip,
        std::function<void(CatalogedType* newItem, const CatalogedType* oldItem)> initializer)
    {
        m_initializer = initializer;

        initFieldsAndSetter(label, tooltip);
        setCurrentIndex(0);
    }

    //! Initialize by means of a catalog class, a subsection of allowed types and an initializer
    //! function.
    //!
    //! Same as before, but only a subset of types available in a catalog shall be used.
    //! The current selection will be initialized with the first type in the given types subset.
    //! Each newly created item can be initialized with the given initializer method. Note that the
    //! item creation will take place also after a call to this method, so take care that the given
    //! initializer stays valid throughout the lifetime of this SelectionProperty instance.
    //!
    //! The initializer function takes two arguments: The first is the new item, the second is the
    //! old one (if present; can be null). This is intended to maybe copy values from the old to the
    //! new selection. The old item also can be ignored, always according to the current
    //! needs.
    void initWithInitializer(
        const QString& label, const QString& tooltip, const QVector<typename Catalog::Type>& types,
        std::function<void(CatalogedType* newItem, const CatalogedType* oldItem)> initializer)
    {
        m_initializer = initializer;
        m_types = types;

        initFieldsAndSetter(label, tooltip);
        setCurrentIndex(0);
    }

    //! Direct access to the stored pointer
    CatalogedType* operator->() const { return m_p.get(); }

    //! Direct access to the stored pointer
    CatalogedType* currentItem() const { return m_p.get(); }

    //! Directly set the new item.
    void setCurrentItem(CatalogedType* t, bool callInitializer = false)
    {
        if (callInitializer && m_initializer)
            m_initializer(t, m_p.get());
        m_p.reset(t);
    }

    //! Directly set the new item.
    template <typename S, typename... ArgsForConstructor>
    S* setCurrentItem(ArgsForConstructor... argsForConstructor)
    {
        S* s = new S(argsForConstructor...);
        if (s != nullptr && m_initializer)
            m_initializer(s, m_p.get());
        m_p.reset(s);
        return s;
    }

    //! Serializes the catalog index of the currently selected type and calls
    //! main serialization method of the selected class.
    void writeTo(QXmlStreamWriter* w) const
    {
        const uint typeIndex = static_cast<uint>(Catalog::type(m_p.get()));
        XML::writeAttribute(w, XML::Attrib::type, typeIndex);
        // The next line allows to see the name of item type in XML. May be skipped while reading.
        XML::writeAttribute(w, XML::Attrib::name,
                            Catalog::uiInfo(Catalog::type(m_p.get())).menuEntry);
        XML::writeAttribute(w, XML::Attrib::selection_version, uint(1));
        if (m_p)
            m_p->writeTo(w);
    }

    //! Deserializes the catalog index of the currently selected type, creates a new
    //! object of this type and calls main deserialization method of the selected class.
    template <typename... ArgsForCreation>
    void readFrom(QXmlStreamReader* r, ArgsForCreation... argsForCreation)
    {
        const uint sel_ver = XML::readUIntAttribute(r, XML::Attrib::selection_version);
        Q_UNUSED(sel_ver)

        const uint typeIndex = XML::readUIntAttribute(r, XML::Attrib::type);
        const auto type = static_cast<typename Catalog::Type>(typeIndex);
        auto* p = Catalog::create(type, argsForCreation...);
        if (p)
            p->readFrom(r);
        m_p.reset(p);
    }

    QString label() const { return m_label; }
    QString tooltip() const { return m_tooltip; }
    QStringList options() const { return m_options; }

    void setCurrentIndex(int newIndex) override { currentIndexSetter(newIndex); }
    int currentIndex() const override { return m_types.indexOf(Catalog::type(m_p.get())); }

private:
    template <typename... ArgsForCreation>
    void initFieldsAndSetter(const QString& label, const QString& tooltip,
                             ArgsForCreation... argsForCreation)
    {
        m_label = label;
        m_tooltip = tooltip;
        m_options.clear();
        for (const auto type : m_types)
            m_options << Catalog::uiInfo(type).menuEntry;

        currentIndexSetter = [this, argsForCreation...](int current) {
            auto* p = Catalog::create(m_types[current], argsForCreation...);
            if (m_initializer)
                m_initializer(p, m_p.get());
            m_p.reset(p);
        };
    }

private:
    std::unique_ptr<CatalogedType> m_p; //!< Current selection

    QString m_label;       //!< A label text (short, no trailing colon)
    QString m_tooltip;     //!< Tooltip text
    QStringList m_options; //!< List of options, usually presented as combo entries
    QVector<typename Catalog::Type> m_types = Catalog::types();

    //! Reimplementable function to set currently selected option.
    std::function<void(int)> currentIndexSetter = nullptr;

    //! initializer function. Can be empty.
    //! The first argument is the new item, the second is the old one if present; can be null.
    //! This is intended to maybe copy values from the old to the new selection. oldItem also can be
    //! ignored if not relevant,
    std::function<void(CatalogedType* newItem, const CatalogedType* oldItem)> m_initializer;
};

//! A special kind of owning vector, providing "standard" interfaces for
//! 'std::vector<SelectionProperty<Catalog>>', used in classes that work not just with single
//! SelectionProperty but with the set of them, for example 'InstrumentModel', 'CompoundItem',
//! 'ParticleLayoutItems', 'MaskItems'

template <typename Catalog>
class SelectionVector {
public:
    using CatalogedType = typename Catalog::CatalogedType;

    void delete_element(const CatalogedType* element)
    {
        for (size_t i = 0; i < m_selections.size(); i++)
            if (m_selections[i].currentItem() == element)
                m_selections.erase(m_selections.begin() + i);
    }

    void delete_at(size_t i) { m_selections.erase(m_selections.begin() + i); }

    void insert_at(size_t i, CatalogedType* element)
    {
        SelectionProperty<Catalog> newSelection;
        newSelection.setCurrentItem(element);
        m_selections.insert(m_selections.begin() + i, std::move(newSelection));
    }

    int index_of(CatalogedType* element) const
    {
        for (size_t i = 0; i < m_selections.size(); i++)
            if (m_selections[i].currentItem() == element)
                return int(i);
        return -1;
    }

    void move(size_t fromIndex, size_t toIndex)
    {
        if (fromIndex > toIndex)
            std::rotate(m_selections.rend() - fromIndex - 1, m_selections.rend() - fromIndex,
                        m_selections.rend() - toIndex);
        else
            std::rotate(m_selections.begin() + fromIndex, m_selections.begin() + fromIndex + 1,
                        m_selections.begin() + toIndex + 1);
    }

    QVector<CatalogedType*> toQVector() const
    {
        QVector<CatalogedType*> result;
        for (const auto& sel : m_selections)
            result.append(sel.currentItem());
        return result;
    }

    void push_back(CatalogedType* item) { insert_at(m_selections.size(), item); }
    void clear() { m_selections.clear(); }
    size_t size() const { return m_selections.size(); }
    bool empty() const { return m_selections.empty(); }
    SelectionProperty<Catalog>& operator[](int i) { return m_selections[i]; }
    SelectionProperty<Catalog>& at(int i) { return m_selections.at(i); }
    SelectionProperty<Catalog>& front() { return m_selections.front(); }
    SelectionProperty<Catalog>& back() { return m_selections.back(); }

    using Iterator = typename std::vector<SelectionProperty<Catalog>>::iterator;
    using ConstIterator = typename std::vector<SelectionProperty<Catalog>>::const_iterator;

    ConstIterator begin() const { return m_selections.cbegin(); }
    ConstIterator end() const { return m_selections.cend(); }
    Iterator begin() { return m_selections.begin(); }
    Iterator end() { return m_selections.end(); }

private:
    std::vector<SelectionProperty<Catalog>> m_selections;
};

#endif // BORNAGAIN_GUI_MODEL_DESCRIPTOR_SELECTIONPROPERTY_H
