//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/Model/Descriptor/PolyItem.h
//! @brief     Defines class PolyItem.
//!
//! @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_POLYITEM_H
#define BORNAGAIN_GUI_MODEL_DESCRIPTOR_POLYITEM_H

#include "GUI/Model/Util/UtilXML.h"

//! Holds a polymorphous item. Possible types of the item are specified by a Catalog.

template <typename Catalog> class PolyItem {
public:
    using BaseType = typename Catalog::BaseType;

    //! 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... Args> void initWithArgs(const QString& label, const QString& tooltip,
                                                  typename Catalog::Type currentType, Args... args)
    {
        initFieldsAndSetter(label, tooltip, args...);
        int index = Catalog::types().indexOf(currentType);
        setCurrentIndex(index);
    }

    //! 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 PolyItem 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(BaseType* newItem, const BaseType* oldItem)> initializer)
    {
        m_initializer = initializer;
        m_types = types;

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

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

    //! Direct access to the stored pointer
    BaseType* certainItem() const { return m_item.get(); }

    //! Directly set the new item.
    void setCertainItem(BaseType* t) { m_item.reset(t); }

    //! Directly set the new item.
    template <typename S> void createCertainItem()
    {
        BaseType* t = new S();
        if (t != nullptr && m_initializer)
            m_initializer(t, m_item.get());
        m_item.reset(t);
    }

    static void writeItemTo(const BaseType* t, QXmlStreamWriter* w)
    {
        const uint typeIndex = static_cast<uint>(Catalog::type(t));
        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(t)).menuEntry);
        XML::writeAttribute(w, XML::Attrib::selection_version, uint(1));
        if (t)
            t->writeTo(w);
    }

    //! Serializes the catalog index of the currently selected type and calls
    //! main serialization method of the selected class.
    void writeTo(QXmlStreamWriter* w) const { writeItemTo(m_item.get(), w); }

    template <typename... Args> BaseType* readItemFrom(QXmlStreamReader* r, Args... args)
    {
        const uint sel_ver = XML::readUInt(r, XML::Attrib::selection_version);
        Q_UNUSED(sel_ver)
        const uint typeIndex = XML::readUInt(r, XML::Attrib::type);
        const auto type = static_cast<typename Catalog::Type>(typeIndex);
        BaseType* t = Catalog::create(type, args...);
        if (t) {
            if (m_initializer)
                m_initializer(t, m_item.get());
            t->readFrom(r);
        }
        return t;
    }

    //! 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... Args> void readFrom(QXmlStreamReader* r, Args... args)
    {
        m_item.reset(readItemFrom(r, args...));
    }

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

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

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

        currentIndexSetter = [this, args...](int current) {
            BaseType* t = Catalog::create(m_types[current], args...);
            if (m_initializer)
                m_initializer(t, m_item.get());
            m_item.reset(t);
        };
    }

    std::unique_ptr<BaseType> m_item; //!< 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(BaseType* newItem, const BaseType* oldItem)> m_initializer;
};

#endif // BORNAGAIN_GUI_MODEL_DESCRIPTOR_POLYITEM_H
