/*****************************************************************************
 * $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$
 ****************************************************************************/

#include "Property.h"

#include <QMetaEnum>
#include <QVector3D>
#include <QVector4D>
#include <QPoint>
#include <QPointF>
#include <QColor>
#include <QJsonObject>
#include <QJsonArray>

namespace camitk {

// -------------------- constructor --------------------
Property::Property(QString name, const QVariant& variant, QString description, QString unit) : initialValue{variant} {
    this->name = name;
    // keep initial null string if parameter is empty or null
    if (!unit.isEmpty() && !unit.isNull()) {
        this->unit = unit;
    }
    this->description = description;
    readOnly = false;
}

Property::Property(Property& p) {
    name = p.getName();
    initialValue = p.getInitialValue();
    description = p.getDescription();
    unit = p.getUnit();
    setReadOnly(p.getReadOnly());
    setGroupName(p.getGroupName());

    if (!p.getEnumTypeName().isNull()) {
        setAttribute("enumNames", p.getAttribute("enumNames").toStringList());
        setEnumTypeName(p.getEnumTypeName());
    }
    else {
        // set constrained attributes
        for (QString attributeName : p.getAttributeList()) {
            if (attributeName == "maximum" || attributeName == "minimum" || attributeName == "singleStep") {
                setAttribute(attributeName, p.getAttribute(attributeName).toDouble());
            }
            else {
                if (attributeName == "decimals") {
                    setAttribute(attributeName, p.getAttribute(attributeName).toInt());
                }
                else {
                    if (attributeName == "regExp") {
                        setAttribute(attributeName, p.getAttribute(attributeName).toString());
                    }
                }
            }
        }
    }
}

// -------------------- getName --------------------
const QString& Property::getName() const {
    return name;
}

// -------------------- getInitialValue --------------------
const QVariant& Property::getInitialValue() const {
    return initialValue;
}

// -------------------- setReadOnly --------------------
void Property::setReadOnly(bool isReadOnly) {
    readOnly = isReadOnly;
}

// --------------------getReadOnly  --------------------
bool Property::getReadOnly() const {
    return readOnly;
}

// -------------------- setDescription --------------------
void Property::setDescription(QString description) {
    this->description = description;
}

// -------------------- getDescription --------------------
const QString& Property::getDescription() const {
    return description;
}

// -------------------- setUnit --------------------
void Property::setUnit(QString unit) {
    this->unit = unit;
}

// -------------------- getUnit --------------------
const QString& Property::getUnit() const {
    return unit;
}

// -------------------- getEnumTypeName --------------------
QString Property::getEnumTypeName() const {
    return enumTypeName;
}

// -------------------- setEnumTypeName --------------------
void Property::setEnumTypeName(QString nameOfTheEnum) {
    enumTypeName = nameOfTheEnum;
}

void Property::setEnumTypeName(QString nameOfTheEnum, QObject* objectDeclaringTheEnum) {
    setEnumTypeName(nameOfTheEnum);

    //-- build enumNames property from enum
    QStringList enumAutoGuiLiterals;
    const QMetaObject* metaObj = objectDeclaringTheEnum->metaObject();
    QMetaEnum enumType = metaObj->enumerator(metaObj->indexOfEnumerator(enumTypeName.toStdString().c_str()));

    // loop over the enum type, get the key value as string and beautify it
    for (int i = 0; i < enumType.keyCount(); i++) {
        // capitalize every word and replace "_" by space
        QStringList tokens = QString(enumType.key(i)).split(QLatin1Char('_'), Qt::SkipEmptyParts);
        for (int j = 0; j < tokens.size(); ++j) {
            tokens[j] = tokens[j].toLower();
            tokens[j].replace(0, 1, tokens[j][0].toUpper());
        }
        enumAutoGuiLiterals << tokens.join(" ");
    }
    setAttribute("enumNames", enumAutoGuiLiterals);
}

// -------------------- getEnumValueAsString --------------------
QString Property::getEnumValueAsString(const QObject* objectDeclaringTheEnum) const {
    if (!enumTypeName.isNull()) {
        int indexOfEnum = objectDeclaringTheEnum->metaObject()->indexOfEnumerator(enumTypeName.toStdString().c_str());
        QMetaEnum enumType = objectDeclaringTheEnum->metaObject()->enumerator(indexOfEnum);
        // property current value
        int val = objectDeclaringTheEnum->property(name.toStdString().c_str()).toInt();
        return enumType.valueToKey(val);
    }
    else {
        return enumTypeName;    // null string
    }
}

// -------------------- getEnumIcons --------------------
QMap< int, QIcon > Property::getEnumIcons() const {
    return enumIcons;
}

// -------------------- setEnumIcons --------------------
void Property::setEnumIcons(const QMap< int, QIcon >& enumIcons) {
    this->enumIcons = enumIcons;
}

// -------------------- getGroupName --------------------
QString Property::getGroupName() const {
    return groupName;
}

// -------------------- setGroupName --------------------
void Property::setGroupName(QString groupName) {
    this->groupName = groupName;
}

// -------------------- getAttribute --------------------
QVariant Property::getAttribute(QString attName) {
    if (attributeValues.contains(attName)) {
        return attributeValues.value(attName);
    }
    else {
        return QVariant();    // invalid QVariant
    }
}

// -------------------- getAttributeList --------------------
QStringList Property::getAttributeList() {
    return attributeValues.keys();
}

// -------------------- setAttribute --------------------
void Property::setAttribute(const QString& attribute, const QVariant& value) {
    // If there is already an item with the key attribute, that item's value is replaced with the given value
    attributeValues.insert(attribute, value);
}

// -------------------- getProperty --------------------
Property* Property::getProperty(QObject* object, QString name) {
    // check if the edited object use a list of camitk::Property
    Property* camitkProp = nullptr;
    bool getPropertyExist = QMetaObject::invokeMethod(object, "getProperty", Qt::DirectConnection,
                            Q_RETURN_ARG(Property*, camitkProp),
                            Q_ARG(QString, name));

    // try harder (in case the edited object was not declared in the camitk namespace)
    if (!getPropertyExist) {
        getPropertyExist = QMetaObject::invokeMethod(object, "getProperty", Qt::DirectConnection,
                           Q_RETURN_ARG(camitk::Property*, camitkProp),
                           Q_ARG(QString, name));
    }

    if (!getPropertyExist) {
        return nullptr;
    }
    else {
        return camitkProp;
    }

}

// -------------------- getPropertyInformation --------------------
QJsonObject Property::getPropertyInformation(QObject* object, QString name) {
    QJsonObject information;
    // check if this is a CamiTK property
    Property* camitkProp = Property::getProperty(object, name);
    if (camitkProp != nullptr) {
        QVariant propertyValue = object->property(name.toUtf8().constData());
        QVariant::Type type = propertyValue.type();
        const char* typeName = QMetaType::typeName(type);

        information.insert("name", name);

        // get the camitk Property to introspect more information
        information.insert("description", camitkProp->getDescription());
        if (camitkProp->getReadOnly()) {
            // do not add it if it is not read only as it is the default
            information.insert("readOnly", camitkProp->getReadOnly());
        }
        if (!camitkProp->getUnit().isNull() && !camitkProp->getUnit().isEmpty()) {
            information.insert("unit", camitkProp->getUnit());
        }
        if (!camitkProp->getGroupName().isNull() && !camitkProp->getGroupName().isEmpty()) {
            information.insert("group", camitkProp->getGroupName());
        }

        QVariant initialValue = camitkProp->getInitialValue();
        if (!camitkProp->getEnumTypeName().isNull()) {
            // this is a enum
            information.insert("type", "enum");
            information.insert("defaultValue", initialValue.toInt());
            QStringList enumNames = camitkProp->getAttribute("enumNames").toStringList();
            if (!enumNames.isEmpty()) {
                information.insert("enumValues", QJsonArray::fromStringList(enumNames));
            }
            else {
                // use Qt Meta object introspection
                QJsonArray enumValues;
                QMetaEnum enumType = object->metaObject()->enumerator(object->metaObject()->indexOfEnumerator(camitkProp->getEnumTypeName().toStdString().c_str()));
                for (unsigned int i = 0; i < enumType.keyCount(); i++) {
                    enumValues.append(enumType.key(i));
                }
                information.insert("enumValues", enumValues);
            }
        }
        else {
            // not an enum: default value should respect JSON bool, number, string
            // support specific Qt type, same as in camitk extensions files
            information.insert("type", typeName);
            switch (type) {
                case QVariant::Bool:
                    information.insert("defaultValue", initialValue.toBool());
                    break;
                case QVariant::Int:
                    information.insert("defaultValue", initialValue.toInt());
                    break;
                case QVariant::Double:
                    information.insert("defaultValue", initialValue.toDouble());
                    break;
                case QVariant::Vector3D:
                case QVariant::Vector4D:
                case QVariant::Color:
                case QVariant::Point:
                case QVariant::PointF:
                    information.insert("defaultValue", QString("%1%2").arg(typeName).arg(Property::getValueAsString(initialValue)));
                    break;
                case QVariant::String:
                default:
                    information.insert("defaultValue", initialValue.toString());
                    break;
            }
            // set constrained attributes
            for (QString attributeName : camitkProp->getAttributeList()) {
                if (attributeName == "maximum" || attributeName == "minimum" || attributeName == "singleStep") {
                    information.insert(attributeName, camitkProp->getAttribute(attributeName).toDouble());
                }
                else {
                    if (attributeName == "decimals") {
                        information.insert(attributeName, camitkProp->getAttribute(attributeName).toInt());
                    }
                    else {
                        if (attributeName == "regExp") {
                            information.insert(attributeName, camitkProp->getAttribute(attributeName).toString());
                        }
                    }
                }
            }
        }
    }

    return information;
}

// -------------------- getValueAsString --------------------
QString Property::getValueAsString(QVariant v) {
    if (!v.isValid()) {
        return QString(); // null String
    }

    // default
    QString result = v.toString();

    // Check for known types QVector3D
    if (v.userType() == qMetaTypeId<QVector3D>()) {
        QVector3D vector = v.value<QVector3D>();
        result = QString("(%1, %2, %3)").arg(vector.x()).arg(vector.y()).arg(vector.z());
    }
    if (v.userType() == qMetaTypeId<QVector4D>()) {
        QVector4D vector = v.value<QVector4D>();
        result = QString("(%1, %2, %3, %4)").arg(vector.w()).arg(vector.x()).arg(vector.y()).arg(vector.z());
    }
    if (v.userType() == qMetaTypeId<QPoint>()) {
        QPoint point = v.value<QPoint>();
        result = QString("(%1, %2)").arg(point.x()).arg(point.y());
    }
    if (v.userType() == qMetaTypeId<QPointF>()) {
        QPointF point = v.value<QPointF>();
        result = QString("(%1, %2)").arg(point.x()).arg(point.y());
    }
    if (v.userType() == qMetaTypeId<QColor>()) {
        QColor color = v.value<QColor>();
        result = color.name();
    }
    else if (v.userType() == QMetaType::QVariantList) {
        QVariantList list = v.toList();
        QStringList stringList;
        // recursively convert each element
        for (const QVariant& element : list) {
            stringList.append(getValueAsString(element));
        }
        result = "[" + stringList.join(", ") + "]";
    }
    else if (v.userType() == QMetaType::QVariantMap) {
        // recursively convert each key-value pair to a string
        QVariantMap map = v.toMap();
        QStringList stringList;
        for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
            stringList.append(it.key() + ": " + getValueAsString(it.value()));
        }
        result = "{" + stringList.join(", ") + "}";
    }

    return result;
}

}
