/***************************************************************************
     qgsproperty.h
     -------------
    Date                 : January 2017
    Copyright            : (C) 2017 by Nyall Dawson
    Email                : nyall dot dawson at gmail dot com
 ***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
#ifndef QGSPROPERTY_H
#define QGSPROPERTY_H

#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgsexpressioncontext.h"

#include <QColor>
#include <QDateTime>
#include <QDomDocument>
#include <QDomElement>
#include <QHash>
#include <QString>
#include <QStringList>
#include <QVariant>

using namespace Qt::StringLiterals;

class QgsPropertyTransformer;
class QgsPropertyPrivate;

/**
 * \ingroup core
 * \class QgsPropertyDefinition
 * \brief Definition for a property.
 *
 * QgsPropertyDefinition defines the type of values allowed for a property, and
 * handles descriptive names and help text for using the property. Definitions
 * can use one of the predefined standard templates to simplify definition of
 * commonly used property types, such as colors and blend modes.
 */
class CORE_EXPORT QgsPropertyDefinition
{
  public:
    //! Predefined standard property templates
    enum StandardPropertyTemplate
    {
      Boolean = 0,                //!< Boolean value
      Integer,                    //!< Integer value (including negative values)
      IntegerPositive,            //!< Positive integer values (including 0)
      IntegerPositiveGreaterZero, //!< Non-zero positive integer values
      Double,                     //!< Double value (including negative values)
      DoublePositive,             //!< Positive double value (including 0)
      Double0To1,                 //!< Double value between 0-1 (inclusive)
      Rotation,                   //!< Rotation (value between 0-360 degrees)
      String,                     //!< Any string value
      Opacity,                    //!< Opacity (0-100)
      RenderUnits,                //!< Render units (eg mm/pixels/map units)
      ColorWithAlpha,             //!< Color with alpha channel
      ColorNoAlpha,               //!< Color with no alpha channel
      PenJoinStyle,               //!< Pen join style
      BlendMode,                  //!< Blend mode
      Point,                      //!< 2D point
      Size,                       //!< 1D size (eg marker radius, or square marker height/width)
      Size2D,                     //!< 2D size (width/height different)
      LineStyle,                  //!< Line style (eg solid/dashed)
      StrokeWidth,                //!< Line stroke width
      FillStyle,                  //!< Fill style (eg solid, lines)
      CapStyle,                   //!< Line cap style (eg round)
      HorizontalAnchor,           //!< Horizontal anchor point
      VerticalAnchor,             //!< Vertical anchor point
      SvgPath,                    //!< Path to an SVG file
      Offset,                     //!< 2D offset
      DateTime,                   //!< DateTime value
      Custom = 3000,              //!< Custom property types
    };

    //! Valid data types required by property
    enum DataType
    {

      /**
       * Property requires a string value. No numeric values are acceptable by the property.
       * Use this for properties which require a string value such as 'dashed' which cannot
       * be stored in a non-string field.
       */
      DataTypeString = 0,

      /**
       * Property requires a numeric value. Note that setting DataTypeNumeric as the required type
       * means that the property also accepts string fields and inputs, as those may be convertible
       * to a numeric value (Eg "1.0" -> 1.0)
       */
      DataTypeNumeric,

      /**
       * Property requires a boolean value. Note that setting DataTypeBoolean as the required type
       * means that the property also accepts string and numeric fields, as those may be convertible
       * to a boolean value (Eg "1.0" -> TRUE)
       */
      DataTypeBoolean,
    };

    /**
     * Constructs an empty property.
     */
    QgsPropertyDefinition() = default;

    /**
     * Constructor for QgsPropertyDefinition, using a standard property template.
     * \param name is used internally and should be a unique, alphanumeric string.
     * \param description can be any localised string describing what the property is used for.
     * \param type one of the predefined standard property template
     * \param origin The origin of the property
     * \param comment A free comment for the property
     */
    QgsPropertyDefinition( const QString &name, const QString &description, StandardPropertyTemplate type, const QString &origin = QString(), const QString &comment = QString() );

    /**
     * Constructor for custom QgsPropertyDefinitions.
     * \param name is used internally and should be a unique, alphanumeric string.
     * \param dataType the data type for the property
     * \param description can be any localised string describing what the property is used for.
     * \param helpText parameter should specify a descriptive string for users outlining the types
     * of value acceptable by the property (eg 'dashed' or 'solid' for a line style property).
     * \param origin The origin of the property
     * \param comment A free comment for the property
     */
    QgsPropertyDefinition( const QString &name, DataType dataType, const QString &description, const QString &helpText, const QString &origin = QString(), const QString &comment = QString() );

    /**
     * Returns the name of the property. This is used internally and should be a unique, alphanumeric string.
     */
    QString name() const { return mName; }

    /**
     * Sets the name of the property
     */
    void setName( const QString &name ) { mName = name; }

    /**
     * Returns the origin of the property. For example, a PAL property has an
     * origin set to "labeling" while a diagram property has an origin set to
     * "diagram".
     */
    QString origin() const { return mOrigin; }

    /**
     * Sets the origin of the property. For example, a PAL property has an
     * origin set to "labeling" while a diagram property has an origin set to
     * "diagram".
     */
    void setOrigin( const QString &origin ) { mOrigin = origin; }

    /**
     * Descriptive name of the property.
     */
    QString description() const { return mDescription; }

    /**
     * Returns the comment of the property
     */
    QString comment() const { return mComment; }

    /**
     * Sets comment of the property
     */
    void setComment( const QString &comment ) { mComment = comment; }

    /**
     * Helper text for using the property, including a description of the valid values for the property.
     */
    QString helpText() const { return mHelpText; }

    /**
     * Sets the data type
     */
    void setDataType( DataType type ) { mTypes = type; }

    /**
     * Returns the allowable field/value data type for the property.
     */
    DataType dataType() const { return mTypes; }

    /**
     * Returns the property's standard template, if applicable. Non standard
     * types will return the Custom template.
     */
    StandardPropertyTemplate standardTemplate() const { return mStandardType; }

    /**
     * Returns TRUE if the property is of a type which is compatible with property
     * override assistants.
     */
    bool supportsAssistant() const;

  private:
    QString mName;
    QString mDescription;
    DataType mTypes = DataTypeString;
    QString mHelpText;
    StandardPropertyTemplate mStandardType = Custom;
    QString mOrigin;
    QString mComment;

    static QString trString();
};


/**
 * \ingroup core
 * \class QgsProperty
 * \brief A store for object properties.
 *
 * QgsProperty objects are used for storing properties for objects, which can then be transformed to
 * a QVariant value by evaluating them against a supplied QgsExpressionContext. Multiple QgsProperty objects
 * can be grouped using a QgsPropertyCollection for easier bulk storage, retrieval and evaluation.
 *
 * QgsProperty objects are implicitly shared and can be inexpensively copied.
 *
 */

class CORE_EXPORT QgsProperty
{
  public:
    /**
     * Convert a map of QgsProperty to a map of QVariant
     * This is useful to save a map of properties
     * \since QGIS 3.20
     */
    static QVariantMap propertyMapToVariantMap( const QMap<QString, QgsProperty> &propertyMap );

    /**
     * Convert a map of QVariant to a map of QgsProperty
     * This is useful to restore a map of properties.
     * The properties are created using QgsProperty::loadVariant
     * \since QGIS 3.20
     */
    static QMap<QString, QgsProperty> variantMapToPropertyMap( const QVariantMap &variantMap );

    /**
     * Constructor for a QgsProperty. The property will be set to an InvalidProperty type.
     */
    QgsProperty();

    ~QgsProperty();

    /**
     * Returns a new ExpressionBasedProperty created from the specified expression.
     */
    static QgsProperty fromExpression( const QString &expression, bool isActive = true );

    /**
     * Returns a new FieldBasedProperty created from the specified field name.
     */
    static QgsProperty fromField( const QString &fieldName, bool isActive = true );

    /**
     * Returns a new StaticProperty created from the specified value.
     */
    static QgsProperty fromValue( const QVariant &value, bool isActive = true );

    QgsProperty( const QgsProperty &other );
    QgsProperty &operator=( const QgsProperty &other );

    /**
     * Returns TRUE if the property is not an invalid type.
     */
    explicit operator bool() const SIP_SKIP;

    bool operator==( const QgsProperty &other ) const;
    bool operator!=( const QgsProperty &other ) const;

    /**
     * Returns the property type.
     */
    Qgis::PropertyType propertyType() const;

    /**
     * Returns whether the property is currently active.
     * \see setActive()
     */
    bool isActive() const;

    /**
     * Returns TRUE if the property is effectively a static value
     * in the specified \a context.
     *
     * I.e. if the property type is QgsProperty::ExpressionBasedProperty with
     * a fixed value expression ('some static value'), this method will return
     * TRUE.
     *
     * \param context expression context
     * \param staticValue will be set to evaluated static value if property is effectively a static value
     * \returns TRUE if property is a static value
     *
     * \since QGIS 3.24
     */
    bool isStaticValueInContext( const QgsExpressionContext &context, QVariant &staticValue SIP_OUT ) const;

    /**
     * Sets whether the property is currently active.
     * \see isActive()
     */
    void setActive( bool active );

    /**
     * Sets the static value for the property. Calling this will
     * transform the property into an StaticProperty.
     * \see staticValue()
     */
    void setStaticValue( const QVariant &value );

    /**
     * Returns the current static value for the property. If the property
     * is not a StaticProperty this will return an invalid variant.
     * \see setStaticValue()
     */
    QVariant staticValue() const;

    /**
     * Sets the field name the property references. Calling this will
     * transform the property into an FieldBasedProperty.
     * \see field()
     */
    void setField( const QString &field );

    /**
     * Returns the current field name the property references. If the property
     * is not a FieldBasedProperty this will return an empty string.
     * \see setField()
     */
    QString field() const;

    /**
     * Sets the expression to use for the property value. Calling this will
     * transform the property into an ExpressionBasedProperty.
     * \see expressionString()
     */
    void setExpressionString( const QString &expression );

    /**
     * Returns the expression used for the property value. If the property
     * is not a ExpressionBasedProperty this will return an empty string.
     * \see setExpressionString()
     */
    QString expressionString() const;

    /**
     * Returns an expression string representing the state of the property, or an empty
     * string if the property could not be converted to an expression
     */
    QString asExpression() const;

    /**
     * Prepares the property against a specified expression context. Calling prepare before evaluating the
     * property multiple times allows precalculation of expensive setup tasks such as parsing expressions.
     * Returns TRUE if preparation was successful.
     */
    bool prepare( const QgsExpressionContext &context = QgsExpressionContext() ) const;

    /**
     * Returns the set of any fields referenced by the property for a specified
     * expression context.
     * \note The optional argument ignoreContext has been added in QGIS 3.14. When set to TRUE,
     * even fields not set in context's fields() will be reported - this is useful e.g. with vector tiles
     * where the actual available field names may not be known beforehand.
     */
    QSet< QString > referencedFields( const QgsExpressionContext &context = QgsExpressionContext(), bool ignoreContext = false ) const;

    /**
     * Returns TRUE if the property is set to a linked project color.
     *
     * \since QGIS 3.6
     */
    bool isProjectColor() const;

    /**
     * Calculates the current value of the property, including any transforms which are set for the property
     * \param context QgsExpressionContext to evaluate the property for. The variables and functions contained
     * in the expression context can be used to alter the calculated value for the property, so that a property
     * is able to respond to the current environment, layers and features within QGIS.
     * \param defaultValue default value to return if the property is not active or cannot be calculated
     * \param ok if specified, will be set to TRUE if conversion was successful
     * \returns calculated value for property
     * \see valueAsString()
     * \see valueAsColor()
     * \see valueAsDouble()
     * \see valueAsInt()
     * \see valueAsBool()
     */
    QVariant value( const QgsExpressionContext &context, const QVariant &defaultValue = QVariant(), bool *ok SIP_OUT = nullptr ) const;

    /**
     * Calculates the current value of the property and interprets it as a datetime.
     * \param context QgsExpressionContext to evaluate the property for.
     * \param defaultDateTime default datetime to return if the property cannot be calculated as a datetime
     * \param ok if specified, will be set to TRUE if conversion was successful
     * \returns value parsed to datetime
     * \see value()
     * \see valueAsString()
     * \see valueAsColor()
     * \see valueAsDouble()
     * \see valueAsInt()
     * \see valueAsBool()
     * \since QGIS 3.14
     */
    QDateTime valueAsDateTime( const QgsExpressionContext &context, const QDateTime &defaultDateTime = QDateTime(), bool *ok SIP_OUT = nullptr ) const;

    /**
     * Calculates the current value of the property and interprets it as a string.
     * \param context QgsExpressionContext to evaluate the property for.
     * \param defaultString default string to return if the property cannot be calculated as a string
     * \param ok if specified, will be set to TRUE if conversion was successful
     * \returns value parsed to string
     * \see value()
     * \see valueAsDateTime()
     * \see valueAsColor()
     * \see valueAsDouble()
     * \see valueAsInt()
     * \see valueAsBool()
     */
    QString valueAsString( const QgsExpressionContext &context, const QString &defaultString = QString(), bool *ok SIP_OUT = nullptr ) const;

    /**
     * Calculates the current value of the property and interprets it as a color.
     * \param context QgsExpressionContext to evaluate the property for.
     * \param defaultColor default color to return if the property cannot be calculated as a color
     * \param ok if specified, will be set to TRUE if conversion was successful
     * \returns value parsed to color
     * \see value()
     * \see valueAsDateTime()
     * \see valueAsString()
     * \see valueAsDouble()
     * \see valueAsInt()
     * \see valueAsBool()
     */
    QColor valueAsColor( const QgsExpressionContext &context, const QColor &defaultColor = QColor(), bool *ok SIP_OUT = nullptr ) const;

    /**
     * Calculates the current value of the property and interprets it as a double.
     * \param context QgsExpressionContext to evaluate the property for.
     * \param defaultValue default double to return if the property cannot be calculated as a double
     * \param ok if specified, will be set to TRUE if conversion was successful
     * \returns value parsed to double
     * \see value()
     * \see valueAsDateTime()
     * \see valueAsString()
     * \see valueAsColor()
     * \see valueAsInt()
     * \see valueAsBool()
     */
    double valueAsDouble( const QgsExpressionContext &context, double defaultValue = 0.0, bool *ok SIP_OUT = nullptr ) const;

    /**
     * Calculates the current value of the property and interprets it as an integer.
     * \param context QgsExpressionContext to evaluate the property for.
     * \param defaultValue default integer to return if the property cannot be calculated as an integer
     * \param ok if specified, will be set to TRUE if conversion was successful
     * \returns value parsed to integer
     * \see value()
     * \see valueAsDateTime()
     * \see valueAsString()
     * \see valueAsColor()
     * \see valueAsDouble()
     * \see valueAsBool()
     */
    int valueAsInt( const QgsExpressionContext &context, int defaultValue = 0, bool *ok SIP_OUT = nullptr ) const;

    /**
     * Calculates the current value of the property and interprets it as an boolean.
     * \param context QgsExpressionContext to evaluate the property for.
     * \param defaultValue default boolean to return if the property cannot be calculated as an boolean
     * \param ok if specified, will be set to TRUE if conversion was successful
     * \returns value parsed to boolean
     * \see value()
     * \see valueAsDateTime()
     * \see valueAsString()
     * \see valueAsColor()
     * \see valueAsDouble()
     * \see valueAsInt()
     */
    bool valueAsBool( const QgsExpressionContext &context, bool defaultValue = false, bool *ok SIP_OUT = nullptr ) const;

    /**
     * Saves this property to a QVariantMap, wrapped in a QVariant.
     * You can use QgsXmlUtils::writeVariant to save it to an XML document.
     *
     * \see loadVariant()
     */
    QVariant toVariant() const;

    /**
     * Loads this property from a QVariantMap, wrapped in a QVariant.
     * You can use QgsXmlUtils::readVariant to load it from an XML document.
     *
     * \see toVariant()
     */
    bool loadVariant( const QVariant &property );

    /**
     * Sets an optional transformer to use for manipulating the calculated values for the property.
     * \param transformer transformer to install. Ownership is transferred to the property, and any
     * existing transformer will be deleted. Set to NULLPTR to remove an existing transformer.
     * \see transformer()
     */
    void setTransformer( QgsPropertyTransformer *transformer SIP_TRANSFER );

    /**
     * Returns the existing transformer used for manipulating the calculated values for the property, if set.
     * \see setTransformer()
     */
    const QgsPropertyTransformer *transformer() const;

    /**
     * Attempts to convert an existing expression based property to a base expression with
     * corresponding transformer. Returns TRUE if conversion was successful. Note that
     * calling this method requires multiple parsing of expressions, so it should only
     * be called in non-performance critical code.
     */
    bool convertToTransformer();

    //! Allows direct construction of QVariants from properties.
    operator QVariant() const { return QVariant::fromValue( *this ); }


#ifdef SIP_RUN
    // clang-format off
    SIP_PYOBJECT __repr__();
    % MethodCode
    QString typeString;
    QString definitionString;
    switch ( sipCpp->propertyType() )
    {
      case Qgis::PropertyType::Static:
        typeString = u"static"_s;
        definitionString = sipCpp->staticValue().toString();
        break;

      case Qgis::PropertyType::Field:
        typeString = u"field"_s;
        definitionString = sipCpp->field();
        break;

      case Qgis::PropertyType::Expression:
        typeString = u"expression"_s;
        definitionString = sipCpp->expressionString();
        break;

      case Qgis::PropertyType::Invalid:
        typeString = u"invalid"_s;
        break;
    }

    QString str = u"<QgsProperty: %1%2%3>"_s.arg( !sipCpp->isActive() && sipCpp->propertyType() != Qgis::PropertyType::Invalid ? u"INACTIVE "_s : QString(),
                  typeString,
                  definitionString.isEmpty() ? QString() : u" (%1)"_s.arg( definitionString ) );
    sipRes = PyUnicode_FromString( str.toUtf8().constData() );
    % End
// clang-format on
#endif

    // clang-format off
    private:
    // clang-format on

    mutable QExplicitlySharedDataPointer<QgsPropertyPrivate>
      d;

    /**
     * Calculates the current value of the property, before any transformations or
     * conversions are applied.
     */
    QVariant propertyValue( const QgsExpressionContext &context, const QVariant &defaultValue = QVariant(), bool *ok = nullptr ) const;
};

Q_DECLARE_METATYPE( QgsProperty )

#endif // QGSPROPERTY_H
