/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK 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 Lesser General Public License version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/

#ifndef PROPERTY_H
#define PROPERTY_H

#include <QVariant>
#include <QStringList>
#include <QIcon>

#include "CamiTKAPI.h"

namespace camitk {

/**
 *
 *  @ingroup group_sdk_libraries_core_utils
 *
 *  @brief
 *  This class describes a property that can be used in components and actions or any class
 *  that needs to be passed to ObjectController.
 *  A property has a type, a description (to be displayed for example as a tooltip,
 *  can be rich-text, see https://doc.qt.io/qt-5/richtext-html-subset.html for supported
 *  html tags), a value, a unit of measurement (SI unit if possible), and some specific attributes (that depends on the type,
 *  e.g. minimal and maximal values, single steps, number of decimals, regular expression...).
 *  An enum type can also be used for properties.
 *
 *  Properties can be grouped in subgroups, see Property::setGroupName().
 *
 *  Basically this is a way to overcome the Qt Meta Object properties limitations.
 *  A camitk::Property enriches a Qt Meta Object property (a very simplified
 *  Decorator Design Pattern).
 *
 *  Using camitk::Property instead of directly using Qt Meta Object property helps
 *  to build a better interactive GUI (in the property explorer for components and
 *  in the ActionWidget for actions).
 *  Note that a camitk::Property is represented as a regular Qt Meta Object property
 *  as well (the value of the camitk::Property is in fact stored by the Qt Meta Object property)
 *
 *  Here are some examples to get started with:
 *  \code
    // Demonstrates how rich text can be used in description
    addParameter(new Property("Bool Prop", false, "This a <i>normal</i> bool property.<br/><b>Note:</b> Rich text description!<br/>See also: <a href=\"http://camitk.imag.fr\">CamiTK web page</a>", ""));

    // beware: the action takes ownership of the Property pointer
    // This means that the line above does not generate memory leak.

    // Demonstrates how properties can be set as read-only
    // Demonstrates how properties can be grouped.
    // Here all read-only properties are assemble in the same eponymic group
    Property *readOnlyBool = new Property("Read Only Bool", true, "This a read-only boolean", "");
    readOnlyBool->setReadOnly(true);
    readOnlyBool->setGroupName("Read Only Properties");
    addParameter(readOnlyBool);

    // beware: the action takes ownership of the Property pointer
    // This means that you should NOT delete readOnlyBool in the action destructor

    // Demonstrates how integer properties can be bounded
    Property *boundedInt = new Property("Bounded Int", 12, "An integer bounded between 0 and 20", "");
    boundedInt->setAttribute("minimum", 0);
    boundedInt->setAttribute("maximum", 20);
    boundedInt->setGroupName("Numeric Properties");
    addParameter(boundedInt);

    // Demonstrates how double properties can be half-bounded
    Property *doubleWithMax = new Property("Double With Max", -10.0, "A double with a max value of -4.2", "");
    doubleWithMax->setAttribute("maximum", -4.2);
    doubleWithMax->setGroupName("Numeric Properties");
    addParameter(doubleWithMax);

    Property *intWithSingleStep = new Property("Int With Single Step", -10, "An integer with a single step of <i>5</i>", "");
    intWithSingleStep->setAttribute("singleStep", 5);
    intWithSingleStep->setGroupName("Numeric Properties");
    addParameter(intWithSingleStep);

    Property *doubleWithStepAndDecimal = new Property("Double With Single Step And Precision", 3.14159, "A double with 5 decimals and a single step of 1.10<sup>-5</sup>", "");
    doubleWithStepAndDecimal->setAttribute("singleStep", 10e-6);
    doubleWithStepAndDecimal->setAttribute("decimals", 5);
    doubleWithStepAndDecimal->setGroupName("Numeric Properties");
    addParameter(doubleWithStepAndDecimal);

    Property *intWithDecimal = new Property("Int With Precision", 4, "An integer with a precision set to 5 decimals: this should not affect it.", "");
    intWithDecimal->setAttribute("decimals", 5);
    intWithDecimal->setGroupName("Numeric Properties");
    addParameter(intWithDecimal);

    Property *readOnlyQVector3D = new Property("Read Only QVector3D", QVector3D(-4.0, 2.0, 0.1), "A read-only QVector3D", "");
    readOnlyQVector3D->setReadOnly(true);
    readOnlyQVector3D->setGroupName("Read Only Properties");
    addParameter(readOnlyQVector3D);

    Property *stringWithRegExp = new Property("QString Constrained by RegExp", QString("loweronly"), "A QString constrained to lowercase characters only (no separators, numbers...)", "");
    stringWithRegExp->setAttribute("regExp", QRegularExpression("[a-z]*"));
    addParameter(stringWithRegExp);

    Property *constrainedQRect = new Property("Constrained QRect", QRect(10,10,20,20), "A QRect constrained to (0,0,50,50)", "");
    constrainedQRect->setAttribute("constraint", QRect(0,0,50,50));
    addParameter(constrainedQRect);

    Property *constrainedQVector3D = new Property("Constrained QVector3D", QVector3D(1.1, 2.2, 3.3), "A constrained QVector3D (not yet implemented)", "");
    constrainedQVector3D->setAttribute("constraint", QVector3D(10.0, 10.0, 10.0));
    addParameter(constrainedQVector3D);
    \endcode
 * @note
 * To create a new Property, prefer using Property.
 * To check if a Property has been added to your Component / Action, use either Component::getProperty() or Action::getProperty() methods.
 * To modify an existing Property's value, check if it exists:
 *  - if not, create a new instance of Property
 *  - if yes, directly modify its value by using QObject::setProperty() method.e
 *
 *  The GUI interaction is automatically build and managed by the class ObjectController.
 *
 *  An example for adding properties to an action can be seen in tutorials/actions/properties.
 *  More specifically see the EnumPropertyExample action to learn about how to use enum properties.
 *
 *  The class PropComponent and PropAction in the tutorials demonstrates how to use camitk::Property
 *  instead of Qt Meta Object Property.
 *
 *  The available property types are:

    Property Type | Property Type Id
    ------------- | -----------------
    int           | QVariant::Int
    double        | QVariant::Double
    bool          | QVariant::Bool
    QString       | QVariant::String
    QVector3D     | QVariant::QVector3D
    QColor        | QVariant::Color
    QDate         | QVariant::Date
    QTime         | QVariant::Time
    QChar         | QVariant::Char
    QDateTime     | QVariant::DateTime
    QPoint        | QVariant::Point
    QPointF       | QVariant::PointF
    QKeySequence  | QVariant::KeySequence
    QLocale       | QVariant::Locale
    QSize         | QVariant::Size
    QSizeF        | QVariant::SizeF
    QRect         | QVariant::Rect
    QRectF        | QVariant::RectF
    QSizePolicy   | QVariant::SizePolicy
    QFont         | QVariant::Font
    QCursor       | QVariant::Cursor
    enum          | enumTypeId()
    flag          | flagTypeId()
    group         | groupTypeId()

    * Possible attributes depends on the property type, mostly (see also QtVariantPropertyManager API doc):
    Property Type | Attribute Name | Attribute Type
    ------------- | :------------: | :------------:
      \c int      |    minimum     | QVariant::Int
      \c int      |    maximum     | QVariant::Int
      \c int      |   singleStep   | QVariant::Int
      \c double   |    minimum     | QVariant::Double
      \c double   |    maximum     | QVariant::Double
      \c double   |   singleStep   | QVariant::Double
      \c double   |    decimals    | QVariant::Int
        QString   |     regExp     | QVariant::RegExp
        QDate     |     minimum    | QVariant::Date
        QDate     |     maximum    | QVariant::Date
        QPointF   |   decimals     | QVariant::Int
        QSize     |    minimum     | QVariant::Size
        QSize     |    maximum     | QVariant::Size
        QSizeF    |    minimum     | QVariant::SizeF
        QSizeF    |    maximum     | QVariant::SizeF
        QSizeF    |    decimals    | QVariant::Int
        QRect     |   constraint   | QVariant::Rect
        QRectF    |   constraint   | QVariant::RectF
        QRectF    |    decimals    | QVariant::Int
       \c enum    |    enumNames   | QVariant::StringList  (note that this can be build automatically)
       \c flag    |   flagNames (NOT IMPLEMENTED YET)  | QVariant::StringList

    \note
    Anywhere in your code use the property(..).toInt() method to get the classical enum value of the property,
    and use Property::getEnumValueAsString() to get the enum value as a string.
    If you declared your enum using Q_ENUM (Qt>=5.5), you can also use directly
    QMetaEnum::fromType to retrieve the enum value as strings (see below).

    \note
    If your property is an action parameter, it is safer to use the dedicated methods
    getParameterValue(..), setParameterValue(..), getParameterValueAsString(..) as they
    will first check that the given parameter name is a declared parameter.
    For component properties, it is safer to use the dedicated methods
    getPropertyValue(..) and setPropertyValue(..) as they will also first check that the
    given property name is a declared property.

    \note
    For enums, you need to do few things in the C++ class that has a enum typed property:
      - add Q_underscore_OBJECT macro in your class declaration
      - either setup a new enum (option 1) or just fill in strings in the enum names (option 2)

    For option 1, you need
    - add the enum type in your class declaration
    - register your enum name using the Q_ENUM macro in your class declaration
    - register the enum type name to the property using the Property::setEnumTypeName (see example below)

    For option 2 (recommended), just
    - create a QStringList with the GUI strings
    - use the Property::setAttribute("enumNames", yourQStringList)

    \note
    Using option 2 is recommended as it will allow you to dynamically update the enum names at any time
    (for instance if you want to select a component, you can update the enum names using the currently
    opened components).

    You can change the enum value names in the GUI using the "enumNames" attributes.
    There is also a way to automatically build nicer enumNames (see below).

    Enum icons might be set using Property::setEnumIcons.

    For instance in the header:
    \code
        class MyAction : public camitk::Action {
            // Really needed! (replace ‗ by _ in your code if you copy-paste this snippet)
            Q‗OBJECT

            //-- option 1
            // declare the C++ enum
            enum MyEnum {
                PossibleValue1,
                PossibleValue2
            };
            // register the enum (Qt >= 5.5)
            Q_ENUM(MyEnum)

            //-- option 2 (recommended)
            // -> nothing to declare in the header
            ...
        };
    \endcode

    And then in the code:
    \code
        MyAction::MyAction(ActionExtension * extension) : Action(extension) {
            ...
            //-- option 1
            // build the dynamic prop based on the enumeration
            Property *enumProp = new Property("My Enumeration", MyAction::PossibleValue2, "Enumeration support example","");
            // register the enum type name for automatically manage the enum as a popup list
            enumProp->setEnumTypeName("MyEnum",this);
            // The Property class automatically build the enum names presented to the user in the GUI
            // (it will changed the enum literals to get a cleaner look, e.g. PossibleValue1 becomes "Possible Value 1")

            // OR

            //-- option 2 (recommended)
            // build the dynamic prop based on a custom string list
            Property *enumProp = new Property("My Enumeration", 0 /-* index of the initial value in the string list *-/, "Enumeration support example","");
            // Set the enum names of your choice, using the enumNames property:
            enumProp->setEnumTypeName("MyEnum");
            // add strings to populate the GUI
            QStringList enumValues;
            enumValues << "Possible Value #1" << "Possible Value #2";
            enumProp->setAttribute("enumNames", enumValues);

            //-- after either option 1 or 2: register the new prop as an action parameter
            addParameter(enumProp);
       }

       ...

        //-- option 1 usage
            // option 1.1: get the value as classical C++ enum
            MyEnum enumPropCurrentValue = (MyEnum) getParameterValue("My Enumeration").toInt();

            // option 1.2: get the value as a QString (either "PossibleValue1" or "PossibleValue2", beware: this is different from the GUI names), you need the enumProp pointer
            QString enumPropAsString = enumProp->getEnumValueAsString(this);

            // option 1.3: using the Q_ENUM declaration in the header, get the value using Qt meta object
            QMetaEnum metaEnum = QMetaEnum::fromType<EnumerationExample>();
            QString enumPropAsStringDirect = metaEnum.valueToKey(enumPropCurrentValue);

        //-- option 2 usage (recommended)
            QString QString enumPropAsString = enumValues.value(getParameterValue("My Enumeration").toInt()
    \endcode

    \note
    This is not exactly a decorator design pattern, as the Property class is not abstract. The Qt
    Meta Object is still held by the QtObject inherited class (e.g. Component or Action).
    The camitk::Property class adds description, readOnly status and specific attributes to
    a QObject dynamic property.
 */
class CAMITK_API Property {

public:
    /** Constructor.
     * The variant parameters also allows you to initialize the value of the property.
     * By default a Property is enabled and editable (i.e. by default it is not read-only)
     * @param name property name (unique identifier of your class property
     * @param variant specify the property type (QVariant) and initial value
     * @param description a sentence or two to describe the property (and its unit if any), can be Rich Text
     * @param unit a unit of measurement (in SI unit), use symbols from https://en.wikipedia.org/wiki/SI_base_unit or https://en.wikipedia.org/wiki/SI_derived_unit when possible
     */
    Property(QString name, const QVariant& variant, QString description, QString unit);

    /// Copy constructor (needed for python module)
    Property(Property&);

    /// Destructor
    virtual ~Property() = default;

    /// get the name of the property
    const QString& getName() const;

    /// return the initial (default) value
    const QVariant& getInitialValue() const;

    /// set this property as read-only
    void setReadOnly(bool);

    /// @return true only if this property is read-only
    bool getReadOnly() const;

    /// set the description (can be rich text)
    void setDescription(QString);

    /// get the description
    const QString& getDescription() const;

    /// set the unit
    void setUnit(QString);

    /// get the unit
    const QString& getUnit() const;

    /** Set a given property for this attribute.
     *  Note that not all the attributes are not usable for all property type (see table above).
     *  The supported attribute names are (see QtVariantPropertyManagerPrivate() constructor):
     *  - "maximum"
     *  - "minimum"
     *  - "singleStep"
     *  - "decimals"
     *  - "constraint"
     *  - "enumNames"
     *  - "regExp"
     *
     * @param attribute name of the attribute
     * @param value value of this attribute
     */
    void setAttribute(const QString& attribute, const QVariant& value);

    /** get the current value of a given attribute, see setAttribute()
     *  if the attribute attName was never set using setAttribute(), the return QVariant
     *  is invalid.
     *  To test if a QVariant is invalid, use the QVariant::isValid() method
     *  e.g.:
     *  if (!myProp.getAttribute("bad").isValid()) {
     *      CAMITK_INFO(tr("myProp does not have an attribute 'bad' (or this attribute is still equals to the default value)"))
     *  }
     *
     *  It is recommended to only use this method inside a foreach(QString s: getAttributeList())
     */
    QVariant getAttribute(QString attName);

    /// returns the list of attribute names that are specific to this property
    QStringList getAttributeList();

    /** if the property's type is an enum, set the name of the registered Qt Enum.
     *  The enum names that will appear in the GUI will have to be given by setting the "enumNames" attribute.
     */
    void setEnumTypeName(QString);

    /** if the property's type is an enum, set the name of the registered Qt Enum AND automatically build
     *  the enum names that will be used in the GUI.
     *  There is no need to set the "enumNames" attribute, the enum names will automatically be build from the enum literals:
     *      - all words will be capitalized
     *      - all "_" (underscore) will be transformed to space
     *
     *  @param enumTypeName the enum type name as declared in the header file
     *  @param objectDeclaringTheEnum is a pointer to the object instantiated from the class that declared the enum
     */
    void setEnumTypeName(QString enumTypeName, QObject* objectDeclaringTheEnum);

    /// @returns the name of the Qt registered enum if the property's type is an enum, the null string otherwise (can be tested against QString isNull() method)
    QString getEnumTypeName() const;

    /** Utility method to get the current property value as a string.
     *  The string corresponds one of the enum values: not the gui enum values set using setAttribute("enumNames"...)
     *
     *  If the property's type is an enum, this is the string corresponding to its value (the property value can be accessed, the normal way using the QVariant toInt() to get the int value (classical C++ enum value)
     * @param objectDeclaringTheEnum is a pointer to the object instantiated from the class that declared the enum
     * @return the enum value as QString.
     */
    QString getEnumValueAsString(const QObject* objectDeclaringTheEnum) const;

    /// get the enum icons
    QMap<int, QIcon> getEnumIcons() const;

    /// set the icons for all the enums
    void setEnumIcons(const QMap<int, QIcon>& enumIcons);

    /** Set the group name.
     *  Properties can be separated into subgroups. Just set the group name and they will be arranged/classified by group.
     *  @param groupName name of the group for this property
     */
    void setGroupName(QString groupName);

    /** get this property subgroup's name
     * @return the group name or the null QString if no group were set, it can be tested with QString::isNull().
     */
    QString getGroupName() const;

    /// @brief get the camitk::Property decoration of a named property of the given QObject or nullptr if object does not have any camitk::Property
    ///
    /// \note
    /// If object is an instance of a class that declares a Q_INVOKABLE getProperty(QString) method, it means that a Qt Property "decoration"
    /// was stored. In this case, this method returns the result of getProperty(name) (i.e., it is expected that the result won't b nullptr
    /// if the named property is found.
    ///
    /// @param object The QObject to check
    /// @param name The property name to look for
    /// @return the pointer to the camitk::Property corresponding to the CamiTK Property of the named property
    static Property* getProperty(QObject* object, QString name);

    /// Get the CamiTK property information as QJsonObject
    /// If name is the name of a CamiTK property of the given object,
    /// this method returns a QJsonObject that contains the following keys:
    /// "name", "type", "defaultValue", "readOnly", "unit"...
    /// @return the CamiTK property information as QJsonObject or an empty QJsonObject if name is not a CamiTK property of object
    static QJsonObject getPropertyInformation(QObject* object, QString name);

    /// Transform a QVariant to a QString representation
    static QString getValueAsString(QVariant v);

private:
    /// name of the property
    QString name;

    /// name of the group in which this property is classified (null if no group name were set)
    QString groupName;

    /** initial value of the property,
     *  Only needed between the time when the property is instantiated and the time it is created by the
     *  Component or Action as a Qt Meta Property. Once the Qt Meta Property is created, the value of
     *  the property is managed by the Qt Meta Property.
     */
    QVariant initialValue;

    /// description of the property, can be rich text
    QString description;

    /// unit (or null string if not specified)
    QString unit;

    /// is the property read only
    bool readOnly;

    /// map containing all the attributes and their values
    QMap<QString, QVariant> attributeValues;

    /// if the property's type is an enum, this is the Qt registered enum name, otherwise it is the null string
    QString enumTypeName;

    /// map containing all the icons for the enum
    QMap<int, QIcon> enumIcons;
};

}

#endif // PROPERTY_H
