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


// -- Core stuff
#include "ObjectController.h"
#include "Property.h"

//-- Qt Property browser stuff
#include "qtvariantproperty.h"
#include "qtgroupboxpropertybrowser.h"
#include "qttreepropertybrowser.h"
#include "qtbuttonpropertybrowser.h"
#include "qtpropertybrowser.h"
#include "qtpropertymanager.h"

// -- QT stuff
#include <QMetaObject>
#include <QMetaProperty>
#include <QVBoxLayout>
#include <QScrollArea>
#include <QRegularExpression>

namespace camitk {
int ObjectControllerPrivate::enumToInt(const QMetaEnum& metaEnum, int enumValue) const {
    QMap<int, int> valueMap; // dont show multiple enum values which have the same values
    int pos = 0;

    for (int i = 0; i < metaEnum.keyCount(); i++) {
        int value = metaEnum.value(i);

        if (!valueMap.contains(value)) {
            if (value == enumValue) {
                return pos;
            }

            valueMap[value] = pos++;
        }
    }

    return -1;
}

int ObjectControllerPrivate::intToEnum(const QMetaEnum& metaEnum, int intValue) const {
    QMap<int, bool> valueMap; // dont show multiple enum values which have the same values
    QList<int> values;

    for (int i = 0; i < metaEnum.keyCount(); i++) {
        int value = metaEnum.value(i);

        if (!valueMap.contains(value)) {
            valueMap[value] = true;
            values.append(value);
        }
    }

    if (intValue >= values.count()) {
        return -1;
    }

    return values.at(intValue);
}

bool ObjectControllerPrivate::isSubValue(int value, int subValue) const {
    if (value == subValue) {
        return true;
    }

    int i = 0;

    while (subValue) {
        if (!(value & (1 << i))) {
            if (subValue & 1) {
                return false;
            }
        }

        i++;
        subValue = subValue >> 1;
    }

    return true;
}

bool ObjectControllerPrivate::isPowerOf2(int value) const {
    while (value) {
        if (value & 1) {
            return value == 1;
        }

        value = value >> 1;
    }

    return false;
}

int ObjectControllerPrivate::flagToInt(const QMetaEnum& metaEnum, int flagValue) const {
    if (!flagValue) {
        return 0;
    }

    int intValue = 0;
    QMap<int, int> valueMap; // dont show multiple enum values which have the same values
    int pos = 0;

    for (int i = 0; i < metaEnum.keyCount(); i++) {
        int value = metaEnum.value(i);

        if (!valueMap.contains(value) && isPowerOf2(value)) {
            if (isSubValue(flagValue, value)) {
                intValue |= (1 << pos);
            }

            valueMap[value] = pos++;
        }
    }

    return intValue;
}

int ObjectControllerPrivate::intToFlag(const QMetaEnum& metaEnum, int intValue) const {
    QMap<int, bool> valueMap; // dont show multiple enum values which have the same values
    QList<int> values;

    for (int i = 0; i < metaEnum.keyCount(); i++) {
        int value = metaEnum.value(i);

        if (!valueMap.contains(value) && isPowerOf2(value)) {
            valueMap[value] = true;
            values.append(value);
        }
    }

    int flagValue = 0;
    int temp = intValue;
    int i = 0;

    while (temp) {
        if (i >= values.count()) {
            return -1;
        }

        if (temp & 1) {
            flagValue |= values.at(i);
        }

        i++;
        temp = temp >> 1;
    }

    return flagValue;
}

void ObjectControllerPrivate::updateClassProperties(const QMetaObject* metaObject, bool recursive) {
    if (!metaObject) {
        return;
    }

    if (recursive) {
        updateClassProperties(metaObject->superClass(), recursive);
    }

    for (int idx = metaObject->propertyOffset(); idx < metaObject->propertyCount(); idx++) {
        QMetaProperty metaProperty = metaObject->property(idx);

        if (metaProperty.isReadable()) {
            if (m_classToIndexToProperty.contains(metaObject) && m_classToIndexToProperty[metaObject].contains(idx)) {
                QtVariantProperty* subProperty = m_classToIndexToProperty[metaObject][idx];

                if (metaProperty.isEnumType()) {
                    if (metaProperty.isFlagType()) {
                        subProperty->setValue(flagToInt(metaProperty.enumerator(), metaProperty.read(m_object).toInt()));
                    }
                    else {
                        subProperty->setValue(enumToInt(metaProperty.enumerator(), metaProperty.read(m_object).toInt()));
                    }
                }
                else {
                    subProperty->setValue(metaProperty.read(m_object));
                }
            }
        }
    }
}

QtVariantProperty* ObjectControllerPrivate::buildQtVariantProperty(QString name, QMetaType::Type type, QVariant value, bool isReadable, bool isWritable, bool isEnumType, bool isFlagType, bool isDesignable, QMetaEnum* metaEnum) {
    QtVariantProperty* buildProperty = 0;

    // hide objectName property
    if (!isReadable) {
        buildProperty = m_readOnlyManager->addProperty(QVariant::String, name);
        buildProperty->setValue(QLatin1String("< Non Readable >"));
    }
    else if (isEnumType) {
        if (isFlagType) {
            buildProperty = m_manager->addProperty(QtVariantPropertyManager::flagTypeId(), name);
            QMap<int, bool> valueMap;
            QStringList flagNames;

            for (int i = 0; i < metaEnum->keyCount(); i++) {
                int enumValue = metaEnum->value(i);

                if (!valueMap.contains(enumValue) && isPowerOf2(enumValue)) {
                    valueMap[enumValue] = true;
                    flagNames.append(QLatin1String(metaEnum->key(i)));
                }

                buildProperty->setAttribute(QLatin1String("flagNames"), flagNames);
                buildProperty->setValue(flagToInt(*metaEnum, value.toInt()));
            }
        }
        else {
            buildProperty = m_manager->addProperty(QtVariantPropertyManager::enumTypeId(), name);
            QMap<int, bool> valueMap; // dont show multiple enum values which have the same values
            QStringList enumNames;

            for (int i = 0; i < metaEnum->keyCount(); i++) {
                int metaEnumValue = metaEnum->value(i);

                if (!valueMap.contains(metaEnumValue)) {
                    valueMap[metaEnumValue] = true;
                    enumNames.append(QLatin1String(metaEnum->key(i)));
                }
            }

            buildProperty->setAttribute(QLatin1String("enumNames"), enumNames);
            buildProperty->setValue(enumToInt(*metaEnum, value.toInt()));
        }
    }
    else if (m_manager->isPropertyTypeSupported(type)) {
        if (!isWritable) {
            buildProperty = m_readOnlyManager->addProperty(type,  name);
        }
        else if (!isDesignable) {
            buildProperty = m_readOnlyManager->addProperty(type, name + " (Non Designable)");
        }
        else {
            buildProperty = m_manager->addProperty(type, name);
        }

        buildProperty->setValue(value);
    }
    else {
        // a map is managed as a group
        if (type == QMetaType::QVariantMap) {
            QString mapName = name;

            buildProperty = m_manager->addProperty(QtVariantPropertyManager::groupTypeId(), mapName);
            // build the subproperties
            QMapIterator<QString, QVariant> it(value.toMap());

            while (it.hasNext()) {
                it.next();
                QtVariantProperty* mapSubProperty = buildQtVariantProperty(it.key(), (QMetaType::Type)it.value().userType(), it.value(), isReadable, isWritable, false, false, isDesignable);

                buildProperty->addSubProperty(mapSubProperty);
            }

        }
        else {
            buildProperty = m_readOnlyManager->addProperty(QVariant::String, name);
            buildProperty->setValue(QLatin1String("< Unknown Type >"));
            buildProperty->setEnabled(false);
        }
    }

    return buildProperty;
}

void ObjectControllerPrivate::addClassProperties(const QMetaObject* metaObject) {
    if (!metaObject) {
        return;
    }

    // CAMITK REMOVED
    //addClassProperties(metaObject->superClass());

    /* CAMITK REMOVED
    QtProperty *classProperty = m_classToProperty.value ( metaObject );
    if ( !classProperty ) {
    QString className = QLatin1String(metaObject->className());
    classProperty = m_manager->addProperty(QtVariantPropertyManager::groupTypeId(), className);
    m_classToProperty[metaObject] = classProperty;
    m_propertyToClass[classProperty] = metaObject;
    */
    for (int idx = 0; idx < metaObject->propertyCount(); idx++) {
        QMetaProperty metaProperty = metaObject->property(idx);

        // hide objectName property
        if (QString(metaProperty.name()) != "objectName") {
            // create the sub property
            QtVariantProperty* subProperty = 0;
            QMetaEnum metaEnum = metaProperty.enumerator();
            subProperty = buildQtVariantProperty(metaProperty.name(), (QMetaType::Type) metaProperty.userType(), metaProperty.read(m_object), metaProperty.isReadable(), metaProperty.isWritable(), metaProperty.isEnumType(), metaProperty.isFlagType(), metaProperty.isDesignable(), &metaEnum);

            /* CAMITK ADDED */
            m_topLevelProperties.append(subProperty);
            m_browser->addProperty(subProperty);

            /* CAMITK REMOVED */
            //classProperty->addSubProperty(subProperty);
            m_classPropertyToIndex[subProperty] = idx;
            m_classToIndexToProperty[metaObject][idx] = subProperty;
        }
    }

    /*      CAMITK REMOVED
        } else {
            updateClassProperties ( metaObject, false );
        }
         m_topLevelProperties.append(classProperty);
         m_browser->addProperty(classProperty);
         */

}

// CAMITK ADDED
void ObjectControllerPrivate::updateDynamicProperties(const QObject* edited) {
    if (!edited) {
        return;
    }

    QList<QByteArray> dynProp = edited->dynamicPropertyNames();

    for (int i = 0; i < dynProp.size(); ++i) {
        int type = edited->property(dynProp.at(i)).userType();
        QtVariantProperty* subProperty = m_indexToDynamicProperty[i];

        // check this is not a hidden property (e.g., "uuid")
        if (subProperty != nullptr) {
            if (m_manager->isPropertyTypeSupported(type)) {
                subProperty->setValue(edited->property(dynProp.at(i)));
            }
            else {
                subProperty->setValue(QLatin1String("< Unknown Type >"));
            }
        }
    }
}

// CAMITK ADDED
// loop on dynamically added property
void ObjectControllerPrivate::addDynamicProperties(QObject* edited) {

    QList<QByteArray> dynProp = edited->dynamicPropertyNames();

    // loop on all dynamic properties
    for (int idx = 0; idx < dynProp.size(); ++idx) {
        QString pName = QString(dynProp.at(idx));
        // Ignore uuid property
        if (pName != "uuid") {
            QVariant pValue = edited->property(dynProp.at(idx));
            QtVariantProperty*  subProperty = 0;
            QtProperty* groupProperty = nullptr;

            // check if the edited object use a list of camitk::Property to
            // add more information about the dynamic property
            Property* camitkProp = Property::getProperty(edited, pName);
            if (camitkProp == nullptr) {
                // the old way, build a simple dynamic variant property
                subProperty = buildQtVariantProperty(pName, (QMetaType::Type) pValue.userType(), pValue, true, /* isWritable*/ true, false, false, true);
            }
            else {
                //-- use all the power of the camitk::Property
                // get then meta enum if available
                if (!camitkProp->getEnumTypeName().isNull()) {
                    const QMetaObject* metaObj = edited->metaObject();
                    QMetaEnum enumType = metaObj->enumerator(metaObj->indexOfEnumerator(camitkProp->getEnumTypeName().toStdString().c_str()));

                    // Create the dynamic variant property + use the read only status
                    subProperty = buildQtVariantProperty(pName, (QMetaType::Type) pValue.userType(), pValue, true, /* isWritable*/ !camitkProp->getReadOnly(), true, false, true, &enumType);
                }
                else {
                    // Create the dynamic variant property + use the read only status
                    subProperty = buildQtVariantProperty(pName, (QMetaType::Type) pValue.userType(), pValue, true, /* isWritable*/ !camitkProp->getReadOnly(), false, false, true);
                }

                // check if we can add icons
                if (!camitkProp->getReadOnly() && !camitkProp->getEnumTypeName().isNull() && camitkProp->getEnumIcons().size() > 0) {
                    subProperty->setAttribute(QString("enumIcons"), camitkProp->getEnumIcons());
                }

                // loop on all property attributes and transfer the values to subProperty
                for (QString attributeName : camitkProp->getAttributeList()) {
                    subProperty->setAttribute(attributeName, camitkProp->getAttribute(attributeName));
                    // if this is an enum, setting the enumNames will reset the value to 0
                    // in case there is not a direct match between names and enum
                    // -> force to enum value again to the current property value so that the GUI matches the current value
                    if (attributeName == "enumNames") {
                        subProperty->setValue(pValue);
                    }
                }

                // use description for tool tip and what's this
                QString description = camitkProp->getDescription();
                QString unit = camitkProp->getUnit();
                if (!unit.isNull() && !unit.isEmpty()) {
                    description += "<br/><b>Unit: </b>" + unit + ".";
                }
                subProperty->setWhatsThis(description);
                subProperty->setToolTip(description);

                // remove rich-text for status tip string (using a reg exp)
                subProperty->setStatusTip(description.remove(QRegularExpression("<[^>]*>")));

                //-- check for subgroup
                if (!camitkProp->getGroupName().isNull()) {
                    // check if the group property already exists
                    groupProperty = groupProperties.value(camitkProp->getGroupName());
                    if (!groupProperty) {
                        // create the group property
                        groupProperty = m_manager->addProperty(QtVariantPropertyManager::groupTypeId(), camitkProp->getGroupName());
                        //m_classToProperty[metaObject] = classProperty;
                        //m_propertyToClass[classProperty] = metaObject;
                        // register it
                        groupProperties.insert(camitkProp->getGroupName(), groupProperty);
                        // add to the top level browser
                        m_topLevelProperties.append(groupProperty);
                        m_browser->addProperty(groupProperty);
                    }
                    groupProperty->addSubProperty(subProperty);
                }
            }

            if (!groupProperty) {
                // add the variant property to the property and build the matching GUI
                m_topLevelProperties.append(subProperty);
                m_browser->addProperty(subProperty);
            }

            m_dynamicPropertyToIndex[subProperty] = idx;
            m_indexToDynamicProperty[idx] = subProperty;
        }
    }


}

void ObjectControllerPrivate::saveExpandedState() {

}

void ObjectControllerPrivate::restoreExpandedState() {

}

void ObjectControllerPrivate::valueChanged(QtProperty* property, const QVariant value) {

    // meta properties
    if (m_classPropertyToIndex.contains(property)) {

        int idx = m_classPropertyToIndex.value(property);

        const QMetaObject* metaObject = m_object->metaObject();
        QMetaProperty metaProperty = metaObject->property(idx);

        if (metaProperty.isEnumType()) {
            if (metaProperty.isFlagType()) {
                metaProperty.write(m_object, intToFlag(metaProperty.enumerator(), value.toInt()));
            }
            else {
                metaProperty.write(m_object, intToEnum(metaProperty.enumerator(), value.toInt()));
            }
        }
        else {
            metaProperty.write(m_object, value);
        }

        updateClassProperties(metaObject, true);
    }

    // dynamic properties
    if (m_dynamicPropertyToIndex.contains(property)) {
        int idx = m_dynamicPropertyToIndex.value(property);
        QList<QByteArray> dynProp = m_object->dynamicPropertyNames();
        QString pName = QString(dynProp.at(idx));
        // Ignore uuid property
        if (pName != "uuid") {
            m_object->setProperty(dynProp.at(idx), value);
            updateDynamicProperties(m_object);
        }
    }
}

void ObjectControllerPrivate::saveChange(QtProperty* property, const QVariant& value) {
    saveChangeValue.insert(property, value);
}

void ObjectControllerPrivate::applyChange() {
    QMapIterator<QtProperty*, QVariant> it(saveChangeValue);

    while (it.hasNext()) {
        it.next();
        valueChanged(it.key(), it.value());
    }

    saveChangeValue.clear();
}

ObjectController::ObjectController(QWidget* parent, ViewMode viewMode)
    : QWidget(parent) {
    d_ptr = new ObjectControllerPrivate;
    d_ptr->q_ptr = this;

    // object unroll in the property browser
    d_ptr->m_object = nullptr;

    // Scroll for view mode GROUPBOX and BUTTON
    scroll = new QScrollArea(this);
    scroll->setWidgetResizable(true);
    scroll->setMinimumSize(300, 100);    // to have no horizontal scrollbar

    // declaration of the layout
    layout = new QVBoxLayout(this);
    layout->setContentsMargins(0, 0, 0, 0);

    // CAMITK ADDED
    currentViewMode = viewMode;
    initViewMode();

    d_ptr->m_readOnlyManager = new QtVariantPropertyManager(this);
    d_ptr->m_manager = new QtVariantPropertyManager(this);
    QtVariantEditorFactory* factory = new QtVariantEditorFactory(this);     //to give a form at every data's type
    d_ptr->m_browser->setFactoryForManager(d_ptr->m_manager, factory);

    setAutoUpdateProperty(false);
}

ObjectController::~ObjectController() {
    delete d_ptr;
}

void ObjectController::setObject(QObject* object) {
    if (d_ptr->m_object == object) {
        return;
    }

    if (d_ptr->m_object) {
        d_ptr->saveExpandedState();
        QListIterator<QtProperty*> it(d_ptr->m_topLevelProperties);

        while (it.hasNext()) {
            d_ptr->m_browser->removeProperty(it.next());
        }

        d_ptr->m_topLevelProperties.clear();

        // all the QtProperty instances are delete, just clear the groupProperties map (no need to delete)
        d_ptr->groupProperties.clear();

    }

    d_ptr->m_object = object;

    if (!d_ptr->m_object) {
        return;
    }

    d_ptr->addClassProperties(d_ptr->m_object->metaObject());
    d_ptr->addDynamicProperties(d_ptr->m_object);

    d_ptr->restoreExpandedState();

    d_ptr->saveChangeValue.clear();
}

QObject* ObjectController::object() const {
    return d_ptr->m_object;
}

// CAMITK ADDED
void ObjectController::setAutoUpdateProperty(bool autoUpdate) {
    if (autoUpdate) {
        connect(d_ptr->m_manager, SIGNAL(valueChanged(QtProperty*, QVariant)),
                this, SLOT(valueChanged(QtProperty*, QVariant)));
    }
    else {
        connect(d_ptr->m_manager, SIGNAL(valueChanged(QtProperty*, QVariant)),
                this, SLOT(saveChange(QtProperty*, QVariant)));
    }
}

void ObjectController::setViewMode(ViewMode viewMode) {
    if (currentViewMode != viewMode) {

        // save the current object show in the property browser to get back later
        QObject* currentObject = object();
        setObject(nullptr);

        if (currentViewMode == TREE) {      // in view mode TREE, the layout contains the property browser
            layout->removeWidget(d_ptr->m_browser);
        }
        else if (currentViewMode == GROUPBOX || currentViewMode == BUTTON) {     // whereas in view mode BUTTON and GROUPBOX, the layout contains the scroll which contains the property browser
            layout->removeWidget(scroll);
        }

        // just delete to re use the same address for the new property browser
        delete d_ptr->m_browser;

        currentViewMode = viewMode;
        initViewMode();

        // reinsert the object
        setObject(currentObject);
    }
}

void ObjectController::initViewMode() {
    switch (currentViewMode) {
        case GROUPBOX :
            d_ptr->m_browser = new QtGroupBoxPropertyBrowser(this);
            layout->addWidget(scroll);
            scroll->setWidget(d_ptr->m_browser);
            break;
        case BUTTON :
            d_ptr->m_browser = new QtButtonPropertyBrowser(this);
            layout->addWidget(scroll);
            scroll->setWidget(d_ptr->m_browser);
            break;
        case TREE :
        default :
            d_ptr->m_browser = new QtTreePropertyBrowser(this);
            dynamic_cast<QtTreePropertyBrowser*>(d_ptr->m_browser)->setRootIsDecorated(false);
            dynamic_cast<QtTreePropertyBrowser*>(d_ptr->m_browser)->setResizeMode(QtTreePropertyBrowser::Interactive);
            layout->addWidget(d_ptr->m_browser);
            break;
    }
}

ObjectController::ViewMode ObjectController::getViewMode() const {
    return currentViewMode;
}

bool ObjectController::isModified() {
    return (!d_ptr->saveChangeValue.empty());
}

void ObjectController::apply() {
    d_ptr->applyChange();
}

void ObjectController::revert() {
    d_ptr->saveChangeValue.clear();
    QObject* currentObject = object();
    setObject(nullptr);
    setObject(currentObject);
}

}

