/*****************************************************************************
 * $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 "QtPropertyFieldEditor.h"

#include <Log.h> // for CAMITK_WARNING

#include <QComboBox>
#include <QCheckBox>
#include <qtpropertybrowserutils_p.h> // required to tweak QtBoolEdit text

// -------------------- Constructor --------------------
QtPropertyFieldEditor::QtPropertyFieldEditor(CamiTKExtensionModelPresenter* presenter, VariantDataModel& dataModel, const QString& name, const QString& description, const QVariant& defaultValue) : FieldEditor(presenter, dataModel, name, description) {
    propManager = nullptr;
    factory = nullptr;
    myProp = nullptr;
    // If the model value is invalid, use the provided value
    // That can happen when the dataModel is not yet created
    initialValue = dataModel.getValue();
    if (!initialValue.isValid()) {
        initialValue = defaultValue;
        dataModel = defaultValue;
    }
    isMandatory = false;
    isTabWidgetTitle = false;
    editableEnums = false;
    comboBoxEditableLineEdit = nullptr;
    labelToUpdate = nullptr;
    decimals = -1;
    isReadOnly = false;
    isTitleCase = false;
    // other QString and QVariant (regExp, placeHolderText, checkBoxText, minimum...) are just declared
    // → the QString is empty/null and the QVariant are invalids
}

// -------------------- setEnumValues --------------------
void QtPropertyFieldEditor::setEnumValues(const QStringList& enumValues) {
    this->enumValues = enumValues;
}

// -------------------- setRegExp --------------------
void QtPropertyFieldEditor::setRegExp(const QString& regExp) {
    this->regExp = regExp;
}

// -------------------- setTitleCase --------------------
void QtPropertyFieldEditor::setTitleCase(bool isTitleCase) {
    this->isTitleCase = isTitleCase;
}

// -------------------- setPlaceHolderText --------------------
void QtPropertyFieldEditor::setPlaceHolderText(const QString& placeHolderText) {
    this->placeHolderText = placeHolderText;
}

// -------------------- setMandatory --------------------
void QtPropertyFieldEditor::setMandatory(bool isMandatory) {
    this->isMandatory = isMandatory;
}

// -------------------- setTabWidgetTitle --------------------
void QtPropertyFieldEditor::setTabWidgetTitle(bool isTabWidgetTitle) {
    this->isTabWidgetTitle = isTabWidgetTitle;
}

// -------------------- setLabelToUpdateWithFieldValue --------------------
void QtPropertyFieldEditor::setLabelToUpdateWithFieldValue(QLabel* labelToUpdate) {
    this->labelToUpdate = labelToUpdate;
}

// -------------------- setEditableEnum --------------------
void QtPropertyFieldEditor::setEditableEnum(bool editableEnums) {
    this->editableEnums = editableEnums;
}

// -------------------- setCheckBoxText --------------------
void QtPropertyFieldEditor::setCheckBoxText(QString checkBoxText) {
    this->checkBoxText = checkBoxText;
}

// -------------------- setMinimum --------------------
void QtPropertyFieldEditor::setMinimum(QVariant minimum) {
    this->minimum = minimum;
    if (myProp != nullptr) {
        myProp->setAttribute("minimum", minimum);
    }
}

// -------------------- setMaximum --------------------
void QtPropertyFieldEditor::setMaximum(QVariant maximum) {
    this->maximum = maximum;
    if (myProp != nullptr) {
        myProp->setAttribute("maximum", maximum);
    }
}

// -------------------- setDecimals --------------------
void QtPropertyFieldEditor::setDecimals(int decimals) {
    this->decimals = decimals;
    if (myProp != nullptr) {
        myProp->setAttribute("decimals", decimals);
    }
}

// -------------------- setReadOnly --------------------
void QtPropertyFieldEditor::setReadOnly(bool isReadOnly) {
    this->isReadOnly = isReadOnly;
}

// -------------------- getWidget --------------------
QWidget* QtPropertyFieldEditor::getWidget() {
    if (propManager == nullptr || factory == nullptr) {
        // initialize property manager and UI factory
        propManager = new QtVariantPropertyManager();
        connect(propManager, &QtVariantPropertyManager::valueChanged, this, &QtPropertyFieldEditor::valueChangedInPropertyField);
        factory = new QtVariantEditorFactory();
        factory->addPropertyManager(propManager);
    }
    if (myWidget == nullptr) {
        if (editableEnums) {
            // add a new empty item at the end before creating the UI
            // required because if the initial value is empty and has a specific
            // meaning (such as in Component Class Name), setting the current index
            // from the initial value (see if statement bellow) would be incorrect
            // (it won't find the value, indexOf will return -1, and the currently
            // selected enum item will be the first one)
            enumValues.append("");
        }
        if (!enumValues.isEmpty()) {
            // now that the empty item is set update the initial value to the current index
            initialValue = enumValues.indexOf(initialValue.toString());
        }
        buildProperty(dataModel.getName(), initialValue, enumValues);
        // check that the QVariant type is supported
        if (myProp != nullptr) {
            if (!regExp.isEmpty()) {
                myProp->setAttribute("regExp", QRegularExpression(regExp));
            }
            if (minimum.isValid()) {
                myProp->setAttribute("minimum", minimum);
            }
            if (maximum.isValid()) {
                myProp->setAttribute("maximum", maximum);
            }
            if (decimals >= 0) {
                myProp->setAttribute("decimals", decimals);
            }

            // QtAbstractEditorFactoryBase always manage to create a widget (myWidget cannot be null after the next line)
            myWidget = ((QtAbstractEditorFactoryBase*)factory)->createEditor(myProp, nullptr);

            // set read only property for all type of widget
            myWidget->setEnabled(!isReadOnly);

            // set specific properties for QComboBox
            QComboBox* enumComboBox = dynamic_cast<QComboBox*>(myWidget);
            if (enumComboBox != nullptr) {
                if (editableEnums) {
                    // setup the editable line edit at the end of the enum
                    comboBoxEditableLineEdit = new QLineEdit();
                    comboBoxEditableLineEdit->setPlaceholderText(placeHolderText);
                    if (!regExp.isEmpty()) {
                        QValidator* validator = new QRegularExpressionValidator(QRegularExpression(regExp));
                        comboBoxEditableLineEdit->setValidator(validator);
                    }
                    enumComboBox->setLineEdit(comboBoxEditableLineEdit);
                    enumComboBox->setEditable(true);

                    // when the user click on the last item, it should become editable
                    connect(enumComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &QtPropertyFieldEditor::currentIndexChanged);
                    // when the user enters a new value in the last item, the data model should be updated
                    connect(enumComboBox, &QComboBox::editTextChanged, this, &QtPropertyFieldEditor::editTextChanged);
                    // if the currently selected index is the last item of the combo box, force the editable mode
                    if (enumComboBox->currentIndex() == enumComboBox->count() - 1) {
                        comboBoxEditableLineEdit->setReadOnly(false);
                    }
                    else {
                        comboBoxEditableLineEdit->setReadOnly(true);
                    }
                }

                // unconditionally filter the mouse wheel events
                enumComboBox->installEventFilter(this);
            }

            // set specific property for QLineEdit
            QLineEdit* lineEdit = dynamic_cast<QLineEdit*>(myWidget);
            if (lineEdit != nullptr) {
                lineEdit->setPlaceholderText(placeHolderText);
                // set nicer looking read only for line edits
                lineEdit->setEnabled(true); // to revoke generic widget read only state (see above)
                lineEdit->setReadOnly(isReadOnly);
                // As setText in
                connect(lineEdit, &QLineEdit::editingFinished, this, [ lineEdit, this ]() {
                    lineEdit->blockSignals(true);
                    lineEdit->setText(dataModel.toString());
                    lineEdit->blockSignals(false);
                });
            }

            // look for a checkbox and set text if provided
            for (QCheckBox* checkBox : myWidget->findChildren<QCheckBox*>()) {
                QtBoolEdit* boolEdit = dynamic_cast<QtBoolEdit*>(myWidget);
                if (boolEdit != nullptr && !checkBoxText.isNull()) {
                    // tweak my widget (it will "believe" the text is not visible and
                    // therefore will not set it back to "True"/"False" when the user click
                    // on the box
                    boolEdit->setTextVisible(false);
                    // Set the text forever
                    checkBox->setText(checkBoxText);
                }
            }

            myWidget->setWhatsThis(description);
            myWidget->setToolTip(description);
        }
        else {
            CAMITK_WARNING(tr("Unsupported type: %1 (%2) cannot be converted to QMetaType::Int").arg(initialValue.typeName()).arg(initialValue.type()));
        }
    }

    return myWidget;
}

// -------------------- eventFilter --------------------
bool QtPropertyFieldEditor::eventFilter(QObject* obj, QEvent* event) {
    if (event->type() == QEvent::Wheel) {
        // If it's a wheel event, ignore it
        return true;
    }
    return QObject::eventFilter(obj, event);
}

// -------------------- valueChangedInPropertyField --------------------
void QtPropertyFieldEditor::valueChangedInPropertyField(QtProperty* property, const QVariant& value) {
    if (myWidget != nullptr) { // to avoid model update when no GUI is setup
        // set the newValue using what the user entered
        QVariant newValue = value;

        QComboBox* enumComboBox = dynamic_cast<QComboBox*>(myWidget);
        if (enumComboBox != nullptr) {
            // get string instead of int value
            newValue = enumComboBox->currentText();
        }
        else {
            QLineEdit* changedLineEdit = dynamic_cast<QLineEdit*>(myWidget);
            // if this is a mandatory QLineEdit, react if it is not empty
            if (changedLineEdit != nullptr) {
                if (isMandatory && value.toString().isEmpty()) {
                    // generate the highlighting stylesheet using the user palette and specific role colors
                    QPalette palette = changedLineEdit->palette();
                    QColor highlightColor = palette.color(QPalette::Highlight);
                    QColor backgroundColor = QColor(255, 220, 168); // #ffdca8
                    QString stylesheet = QString(
                                             "QLineEdit {"
                                             "border: 2px solid %1;"
                                             "background-color: %2;"
                                             "}"
                                         ).arg(highlightColor.name()).arg(backgroundColor.name());

                    changedLineEdit->setStyleSheet(stylesheet);
                    presenter->emitShowMessage("⚠ " + name + " cannot be empty!", 10000, true);
                }
                else {
                    if (!changedLineEdit->styleSheet().isEmpty()) {
                        changedLineEdit->setStyleSheet("");
                        presenter->emitShowMessage("");
                    }
                }

                // Transform to title case if needed
                if (isTitleCase) {
                    QStringList words = value.toString().split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
                    QStringList capitalizedWords;
                    QString firstLetter;
                    bool isDigit;
                    for (const QString& word : words) {
                        firstLetter = word.left(1);
                        firstLetter.toInt(&isDigit);
                        if (firstLetter.isUpper() || isDigit) {
                            // word starts with an upper case letter or a digit, just copy it (e.g. ITK → ITK, CamiTK → CamiTK, 3D → 3D...)
                            capitalizedWords << word;
                        }
                        else {
                            // capitalize first letter, lowercase the end of the word
                            capitalizedWords << firstLetter.toUpper() + word.mid(1).toLower();
                        }
                    }
                    // enforce format to title case
                    newValue = capitalizedWords.join(" ");
                    // Note: UI cannot be updated here (changedLineEdit->setText(..) does nothing)
                    // UI update should be done when focus is lost (see the connect to editingFinished() in getWidget)
                }
            }
        }

        // update parent tab label if required
        if (isTabWidgetTitle) {
            QTabWidget* parentTabWidget = findParentTabWidget();
            if (parentTabWidget != nullptr) {
                parentTabWidget->setTabText(parentTabWidget->currentIndex(), newValue.toString());
            }
        }

        // update the synchronized label if defined
        if (labelToUpdate != nullptr) {
            labelToUpdate->setText(newValue.toString());
        }

        dataModel = newValue;
        emit valueChanged(dataModel);
        presenter->modelUpdated();
    }
}

// -------------------- editTextChanged --------------------
void QtPropertyFieldEditor::editTextChanged(const QString& newValue) {
    // The widget is an editable enum
    dataModel = newValue;
    emit valueChanged(dataModel);
    presenter->modelUpdated();
}

// -------------------- currentIndexChanged --------------------
void QtPropertyFieldEditor::currentIndexChanged(int index) {
    QComboBox* enumComboBox = dynamic_cast<QComboBox*>(myWidget);
    if (enumComboBox != nullptr) {
        // Make the line edit editable for the last item only
        if (index == enumComboBox->count() - 1) {
            comboBoxEditableLineEdit->setReadOnly(false);
        }
        else {
            comboBoxEditableLineEdit->setReadOnly(true);
        }
    }
}

// -------------------- buildProperty --------------------
void QtPropertyFieldEditor::buildProperty(QString name, QVariant value, QStringList enumValues) {
    myProp = nullptr;

    if (!enumValues.isEmpty()) {
        myProp = propManager->addProperty(QtVariantPropertyManager::enumTypeId(), name);
        myProp->setAttribute("enumNames", enumValues);
        myProp->setValue(value);
    }
    else {
        if (propManager->isPropertyTypeSupported(value.userType())) {
            myProp = propManager->addProperty(value.userType(), name);
            myProp->setValue(value);
        }
        else {
            // simplify (conversion from QJSON can create QMetaType::LongLong for instance)
            if (value.canConvert<int>()) {
                // convert to int
                int valueType = QMetaType::Int;
                if (propManager->isPropertyTypeSupported(valueType)) {
                    myProp = propManager->addProperty(valueType, name);
                    myProp->setValue(value.toInt());
                }
            }
        }
    }
}

// -------------------- findParentTabWidget --------------------
QTabWidget* QtPropertyFieldEditor::findParentTabWidget() {
    // Start with myWidget so that if it is not yet initialize
    // it won't result in an infinite loop
    // If we call getWidget() here, as setValue(..) in buildProperty will
    // also call the valueChangedInPropertyField, and therefore this method,
    // this will result in an infinite loop...
    QWidget* widget = myWidget;
    QTabWidget* parentTab = nullptr;
    while (widget != nullptr && parentTab == nullptr) {
        parentTab = qobject_cast<QTabWidget*>(widget);
        widget = widget->parentWidget();
    }
    return parentTab;
}
