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

#include <QString>
#include <QMap>
#include <QVariant>
#include <QList>
#include <QMetaObject>
#include <QMetaProperty>
#include <QMetaType>
#include <QSettings>
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>

#include "Component.h"
#include "CamiTKFile.h"
#include "Application.h"
#include "TransformationManager.h"
#include "Viewer.h"
#include "MainWindow.h"
#include "PropertyObject.h"
#include "HistoryItem.h"
#include "Action.h"
#include "Log.h"

namespace camitk {

// -------------------- saveWorkspace --------------------
bool PersistenceManager::saveWorkspace(QString filepath) {
    // Get the workspace filepath to compute relative paths for components
    QFileInfo fileInfo(filepath);

    // Create a CamiTKFile
    CamiTKFile file;

    // Save Frames and Transformations data to it
    file.addContent("transformationManager", TransformationManager::toVariant());

    // Add all components to it
    file.addContent("components", fromComponents(Application::getTopLevelComponents(), fileInfo.absoluteDir()));

    // Add all actions that were used
    // From History, get all HistoryItems, get Action names
    // Get those actions and save their properties if they exist
    auto history = Application::getHistory();
    QVariantMap appliedActions;
    for (const HistoryItem& item : history) {
        QString actionName = item.getName();
        if (! appliedActions.contains(actionName)) {
            Action* action = Application::getAction(actionName);
            if (action != nullptr) {
                QVariant actionVariant = action->toVariant();
                if (actionVariant.isValid()) {
                    appliedActions.insert(actionName, actionVariant);
                }
            }
        }
    }
    file.addContent("actions", appliedActions);


    // Add all active viewers
    QVariantMap viewersProperties;
    for (Viewer* v : Application::getViewers()) {
        const PropertyObject* viewerPropertyObject = v->getPropertyObject();
        if (viewerPropertyObject != nullptr) {
            QVariant viewerPropertiesVariant = viewerPropertyObject->toVariant();
            if (viewerPropertiesVariant.isValid()) { // Do not save if the viewer does not have properties to save
                viewersProperties.insert(v->getName(), viewerPropertiesVariant);
            }
        }
    }
    file.addContent("viewers", QVariant(viewersProperties));

    // Add Application properties
    // TODO Not all data (stored in settings) is there (e.g. maxRecentDocuments) -> use InterfacePersistence
    const PropertyObject* applicationProperties = Application::getPropertyObject();
    if (applicationProperties != nullptr) {
        file.addContent(Application::getName(), applicationProperties->toVariant());
    }

    // Add MainWindow properties // TODO save only relevant properties
    const MainWindow* window = Application::getMainWindow();
    if (window != nullptr) {
        file.addContent("mainwindow", fromProperties(window));
    }

    // Write the file
    return file.save(filepath);
}

// -------------------- loadWorkspace --------------------
bool PersistenceManager::loadWorkspace(QString filepath) {
    // Load a CamiTK file
    CamiTKFile file = CamiTKFile::load(filepath);
    if (!file.isValid()) {
        CAMITK_WARNING_ALT("Cannot load CamiTK file " + filepath);
        return false;
    }

    // Is the version supported ?
    if (file.getVersion().toStdString() != CamiTKFile::version) {
        CAMITK_WARNING_ALT("Cannot load CamiTK file " + filepath + "\nUnsupported file format version " + file.getVersion());
        return false;
    }

    QFileInfo fileInfo(filepath);

    // Load frames and transformations
    if (file.hasContent("transformationManager")) {
        TransformationManager::fromVariant(file.getContent("transformationManager"));
    }

    // Load components
    if (file.hasContent("components")) {
        loadComponents(file.getContent("components"), fileInfo.absoluteDir());
    }

    // Load action's properties // DISABLED for now, too many side effects (e.g. setting null components)
    /* if (file.hasContent("actions")) {
        QVariantMap actionsMap = file.getContent("actions").toMap();
        for(QString& actionName : actionsMap.keys()){
            Action *action = Application::getAction(actionName);
            if(action != nullptr){
                loadProperties(action, actionsMap.value(actionName));
            }
        }
    }*/

    // Load viewers' properties
    if (file.hasContent("viewers")) {
        QVariantMap viewersMap = file.getContent("viewers").toMap();
        for (QString& viewerName : viewersMap.keys()) {
            Viewer* viewer = Application::getViewer(viewerName);
            if (viewer != nullptr) {
                viewer->getPropertyObject()->fromVariant(viewersMap.value(viewerName));
            }
        }
    }

    // Load Application properties
    if (file.hasContent(Application::getName())) {
        Application::getPropertyObject()->fromVariant(file.getContent(Application::getName()));
    }

    // Load MainWindow properties
    MainWindow* window = Application::getMainWindow();
    if (file.hasContent("mainwindow") && window != nullptr) {
        loadProperties(window, file.getContent("mainwindow"));
    }

    return true;
}

// -------------------- fromComponents --------------------
QVariant PersistenceManager::fromComponents(QList<Component*> comps, QDir rootPath) {
    QVariantList compsVariant;
    for (Component* c : comps) {
        QVariant compVariant = c->toVariant();
        if (compVariant.userType() == QMetaType::QVariantMap) {
            QVariantMap compVariantMap = compVariant.toMap();
            if (compVariantMap.contains("filename")) {
                compVariantMap.insert("filename", rootPath.relativeFilePath(compVariantMap.value("filename").toString()));
            }
            compsVariant.push_back(QVariant(compVariantMap));
        }
        else {
            compsVariant.push_back(compVariant);
        }
    }
    return compsVariant;
}

// -------------------- loadComponents --------------------
bool PersistenceManager::loadComponents(QVariant comps, QDir rootPath) {
    if (comps.userType() != QMetaType::QVariantList) {
        CAMITK_WARNING_ALT("CamiTKFile components is not a list !");
        return false;
    }

    for (QVariant& comp : comps.toList()) {
        // For each component, we open it first from its filepath, then we ask it to load itself
        if (comp.userType() == QMetaType::QVariantMap && comp.toMap().contains("filename") && !comp.toMap().value("filename").toString().isEmpty()) { // Just ignore it if it is invalid
            Component* c = Application::open(rootPath.absoluteFilePath(comp.toMap().value("filename").toString()), true);
            if (!c) {
                CAMITK_WARNING_ALT("Could not load component from filename " + comp.toMap().value("filename").toString());
            }
            else {
                c->fromVariant(comp);
            }
        }
    }
    return true;
}

// -------------------- fromProperties --------------------
QVariant PersistenceManager::fromProperties(const QObject* qobj) {
    if (qobj == nullptr) {
        return QVariant();
    }

    QVariantMap properties;
    const QMetaObject* metaObj = qobj->metaObject();
    // Static properties (declared in the object class using QT_PROPERTY macro)
    for (int i = 0; i < metaObj->propertyCount(); ++i) {
        auto prop = metaObj->property(i);
        // Only writable properties
        if (prop.isWritable() && QString(prop.name()) != "objectName") {
            // try to convert specific known types
            QString convertedValue = variantToString(prop.read(qobj));
            if (convertedValue.isNull()) {
                // conversion was not possible
                properties[prop.name()] = prop.read(qobj);
            }
            else {
                // specific types was found, store it as a QString (that has a specific format)
                properties[prop.name()] = convertedValue;
            }
        }
    }
    // Dynamic properties, added at runtime using QObject::setProperty and/or camitk::Property
    auto dynPropsNames = qobj->dynamicPropertyNames();
    for (auto name : dynPropsNames) {
        auto value = qobj->property(name);
        // We know that invoking getProperty won't change our const object, but the function itself does not accept a const
        // --> using a const_cast
        Property* camitkProperty = Property::getProperty(const_cast<QObject*>(qobj), QString(name));
        if (camitkProperty == nullptr || !camitkProperty->getReadOnly()) {
            // try to convert specific known types
            QString convertedValue = variantToString(value);
            if (convertedValue.isNull()) {
                // conversion was not possible
                properties[QString(name)] = value;
            }
            else {
                // specific types was found, store it as a QString (that has a specific format)
                properties[QString(name)] = convertedValue;
            }
        }
    }
    if (properties.isEmpty()) {
        return QVariant();
    }
    else {
        return properties;
    }
}

// -------------------- loadProperties --------------------
void PersistenceManager::loadProperties(QObject* qobj, QVariant props) {
    QVariantMap properties = props.toMap();
    auto dynPropsNames = qobj->dynamicPropertyNames();

    for (QString& prop : properties.keys()) {
        QVariant oldValue = qobj->property(prop.toStdString().c_str());
        if (oldValue.isValid()) {
            // Property exists
            QVariant newValue = properties.value(prop);
            updateVariantValueWhilePreservingType(oldValue, newValue, prop);
            // oldValue was filled with newValue converted to the right type
            qobj->setProperty(prop.toStdString().c_str(), oldValue);
        }
        else {
            // new property
            qobj->setProperty(prop.toStdString().c_str(), properties.value(prop));
        }
    }
}

// -------------------- getUuidFromProperties --------------------
QUuid PersistenceManager::getUuidFromProperties(const QObject* qobj) {
    QVariant uuid = qobj->property("uuid");
    if (!uuid.isValid()) {
        return QUuid();
    }
    else {
        return uuid.toUuid();
    }
}

// -------------------- setUuidInProperties --------------------
bool PersistenceManager::setUuidInProperties(QObject* qobj, QUuid uuid) {
    QVariant currentId = qobj->property("uuid");
    // If a valid value already exists, do not set !
    if (!currentId.isValid() || currentId.toUuid().isNull()) {
        qobj->setProperty("uuid", QVariant(uuid));
        return true;
    }
    else {
        return false;
    }

}

// -------------------- updateVariantValueWhilePreservingType --------------------
void PersistenceManager::updateVariantValueWhilePreservingType(QVariant& variant, QVariant& newValue, QString name) {
    QVariant convertedValue;
    //If the variant is a QVariantList, replace the current list
    switch (newValue.userType()) {
        case QMetaType::QVariantList:
            if (variant.userType() == QMetaType::QVariantList) {
                // Does the old variant have the same type ?
                if (variant.toList().size() > 0) {
                    // If there is an element in there, use its type
                    int targetTypeId = variant.toList()[0].userType();
                    QVariantList newList = newValue.toList();
                    for (QVariant& v : newList) {
                        v.convert(targetTypeId);
                    }
                    // Now replace the old list
                    variant = newList;
                }
                else { // No known type, just copy !
                    variant = newValue;
                }
            }
            else {
                // the old variant is not a list ! Warn but store it anyway
                CAMITK_TRACE_ALT(QString("Updating the value of '%1' of original type %2, to type QVariantList. Original type will not be preserved.").arg(name).arg(variant.userType()));
                variant = newValue;
            }
            break;
        case QMetaType::QVariantMap :
            // If the variant is a QVariantMap call recursively for all values
            if (variant.userType() == QMetaType::QVariantMap) {
                QVariantMap newMap = newValue.toMap();
                QVariantMap oldMap = variant.toMap();
                for (QString& key : newMap.keys()) {
                    if (oldMap.contains(key)) {
                        // The key is already there, try to keep the same type
                        updateVariantValueWhilePreservingType(oldMap[key], newMap[key], key);
                    }
                    else {
                        // The key is not there, just insert a copy of the new item;
                        oldMap[key] = newMap[key];
                    }
                }
                // Replace oldVariant with updated map
                variant = oldMap;
            }
            else {
                // old variant is not a QVariantMap -> warn and replace
                CAMITK_TRACE_ALT(QString("Updating the value of '%1' of original type %2, to type QVariantMap. Original type will not be preserved.").arg(name).arg(variant.userType()));
                variant = newValue;
            }
            break;

        case QMetaType::QString:
            // Try to convert to specific supported types
            convertedValue = stringToVariant(newValue.toString());
            if (convertedValue.isValid()) {
                newValue = convertedValue;
            }
        // Do NOT break: if the string was converted to a Variant of an unexpected type, go on trying to convert it in 'default'

        default:
            // Try to convert
            if (newValue.canConvert(variant.userType())) {
                convertedValue = newValue;
                convertedValue.convert(variant.userType());
                variant = convertedValue;
            }
            else {
                CAMITK_TRACE_ALT(QString("Updating the value of '%1' of original type %2 (original value '%3'), to type %4 (new value: '%5'). Original type will not be preserved.").arg(name).arg(variant.userType()).arg(variant.toString()).arg(newValue.userType()).arg(newValue.toString()));
                variant = newValue;
            }
    }
}

// -------------------- variantToString --------------------
QString PersistenceManager::variantToString(const QVariant& variant) {
    QString value;
    // Check for supported types
    switch (variant.userType()) {
        case QMetaType::QRect: {
            QRect r = variant.toRect();
            value = QString::asprintf("@Rect(%d %d %d %d)", r.x(), r.y(), r.width(), r.height());
            break;
        }
        case QMetaType::QByteArray: {
            QByteArray a = variant.toByteArray();
            value = QLatin1String("@ByteArray(") + QLatin1String(a.constData(), a.size()) + QLatin1Char(')');
            break;
        }
        default:
            // nothing to do the value.isNull() is true
            break;
    }
    return value;
}

// -------------------- stringToVariant --------------------
QVariant PersistenceManager::stringToVariant(QString value) {
    QVariant variant;

    // Check the different supported types
    if (value.startsWith("@Rect(")) {
        // the destination type is QMetaType::QRect, check if we can convert using custom conversion
        QRegularExpression regex("\\((\\d+)(?:\\s*)(\\d+)(?:\\s*)(\\d+)(?:\\s*)(\\d+)\\)");
        // Check if the regex matches the input string
        QRegularExpressionMatch match = regex.match(value);
        if (match.hasMatch()) {
            // Extract QRect integer values from the captured groups
            variant = QVariant(QRect(match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt(), match.captured(4).toInt()));
        }
    }
    else {
        if (value.startsWith(QLatin1String("@ByteArray("))) {
            // The destination type is QMetaType::QByteArray:
            // Extract the raw data inside the (..)
            variant = QVariant(value.mid(11, value.size() - 12).toLatin1());
        }
    }
    return variant;
}


} //namespace
