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

#include <QJsonArray>
#include <QFile>
#include <QFileInfo>

#include <cmath> // for std::floor
#include <iostream>

/*
https://regex101.com/

@if@ contains($field1$) data contains Field1 @else@ data does not contain field1 @endif@
@if@ contains($field1.field2$) data contains Field1 @else@ data does not contain field1 @endif@
@if@ $field1$ data contains Field1 @else@ data does not contain field1 @endif@
@if@ $field1.field2$ data contains Field1 @else@ data does not contain field1 @endif@
@for@ $p$ in $param$
@if@ contains($field1$) data contains Field1 @else@ data does not contain field1 @endif@
@if@ contains($field1.field2$) data contains Field1 @else@ data does not contain field1 @endif@
@if@ $field1$ data contains Field1 @else@ data does not contain field1 @endif@
@if@ $field1.field2$ data contains Field1 @else@ data does not contain field1 @endif@
@subfor@ $e$ in $tab$
@if@ contains($field1$) data contains Field1 @else@ data does not contain field1 @endif@
@if@ contains($field1.field2$) data contains Field1 @else@ data does not contain field1 @endif@
@if@ $field1$ data contains Field1 @else@ data does not contain field1 @endif@
@if@ $field1.field2$ data contains Field1 @else@ data does not contain field1 @endif@
@endsubfor@
@endfor@
*/
// -------------------- constructor --------------------
TransformEngine::TransformEngine() : templateString("") {
    currentDate = QDate::currentDate();
    currentTime = QTime::currentTime();
    indexInLoop = indexInSubLoop = -1;
}

TransformEngine::TransformEngine(QDate currentDate, QTime currentTime) : TransformEngine() {
    this->currentDate = currentDate;
    this->currentTime = currentTime;
}

// -------------------- setTemplateFile --------------------
bool TransformEngine::setTemplateString(QString templateString) {
    // TODO validate template syntax of templateFile and return false if a problem occurs
    this->templateString = templateString;

    return true;
}

// -------------------- transformToFile --------------------
bool TransformEngine::transformToFile(const QJsonObject& data, const QString& filename, bool overwrite) {
    // transform the current template and the filename then write to file
    return saveToFile(transformToString(templateString, data), transformToString(filename, data), overwrite);
}

// -------------------- saveToFile --------------------
bool TransformEngine::saveToFile(const QString& content, const QString& fileName, bool overwrite) {
    QFile file(fileName);
    QString baseName = QFileInfo(fileName).fileName();

    if (!overwrite && file.exists()) {
        return false;
    }

    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        std::cerr << "TransformEngine error: \"" << fileName.toStdString() << "\" cannot be opened: " << file.errorString().toStdString();
        return false;
    }

    QTextStream out(&file);
    out << content;

    file.close();
    return true;
}

// -------------------- transformToString --------------------
QString TransformEngine::transformToString(const QString& templateString, const QJsonObject& data) {
    QString result;

    result = transformLoop(templateString, data);
    result = transformCondition(result, data);
    result = transformVariable(result, data);
    result = transformFunctions(result, data);
    return result;
}

// -------------------- genericTransform --------------------
QString TransformEngine::genericTransform(const QString& textToTransform, const QString& regularExpression, const std::function<QString(const QStringList&)>& matchFunction, bool withoutTrailingSpace) {
    QStringList transformedTextList;
    QRegularExpression conditionVarFieldRegexp(regularExpression, QRegularExpression::MultilineOption);

    int startIndex = 0;
    QRegularExpressionMatch match = conditionVarFieldRegexp.match(textToTransform);

    while (match.hasMatch()) {
        if (withoutTrailingSpace) {
            // add the text between two matches, and get the appended string so that the indentation can be extracted
            QString transformResult = matchFunction(match.capturedTexts());
            QString appendedIndentation = "";
            if (!transformResult.isNull()) {
                appendedIndentation = extractLeadingSpaces(append(transformedTextList, textToTransform, startIndex, match.capturedStart()));
                appendWithoutTrailingSpace(transformedTextList, /*appendedIndentation +*/ transformResult);
            }
            else {
                appendedIndentation = extractLeadingSpaces(appendWithoutTrailingSpace(transformedTextList, textToTransform, startIndex, match.capturedStart()));
            }
        }
        else {
            append(transformedTextList, textToTransform, startIndex, match.capturedStart());
            append(transformedTextList, matchFunction(match.capturedTexts()));
        }

        startIndex = match.capturedEnd();
        match = conditionVarFieldRegexp.match(textToTransform, startIndex);
    }

    if (withoutTrailingSpace) {
        // Add the remaining substring after the last match if not empty
        appendWithoutTrailingSpace(transformedTextList, textToTransform, startIndex);
    }
    else {
        append(transformedTextList, textToTransform, startIndex);
    }

    return transformedTextList.join("");
}

// -------------------- transformConditionInLoop --------------------
QString TransformEngine::transformConditionInLoop(const QString& loopContent, const QJsonObject& loopContentData, const QString& variableName) {
    /* https://regex101.com/

    test string:
    @if@ equals($field1$,"value") data contains Field1 @else@ data does not contain field1 @endif@
    @if@ equals($field1.field2$,"value") data contains Field1 @else@ data does not contain field1 @endif@
    @if@ contains($field1$) data contains Field1 @else@ data does not contain field1 @endif@
    @if@ contains($field1.field2$) data contains Field1 @else@ data does not contain field1 @endif@
    @if@ $field1$ data contains Field1 @else@ data does not contain field1 @endif@
    @if@ $field1.field2$ data contains Field1 @else@ data does not contain field1 @endif@
    @for@ $p$ in $param$
    @if@ contains($field1$) data contains Field1 @else@ data does not contain field1 @endif@
    @if@ contains($field1.field2$) data contains Field1 @else@ data does not contain field1
    @endif@
    @if@ equals($field1$,"value") data contains Field1 @else@ data does not contain field1 @endif@
    @if@ equals($field1.field2$,"value") data contains Field1 @else@ data does not contain field1 @endif@
    @if@ $field1$ data contains Field1 @else@ data does not contain field1 @endif@
    @if@ $field1.field2$ data contains Field1 @else@ data does not contain field1 @endif@
    @subfor@ $e$ in $tab$
    @if@ contains($field1$) data contains Field1 @else@ data does not contain field1 @endif@
    @if@ contains($field1.field2$) data contains Field1 @else@ data does not contain field1 @endif@
    @if@ $field1$ data contains Field1 @else@ data does not contain field1 @endif@
    @if@ $field1.field2$ data contains Field1 @else@ data does not contain field1 @endif@
    @if@ equals($field1$,"value") data contains Field1 @else@ data does not contain field1 @endif@
    @if@ equals($field1.field2$,"value") data contains Field1 @else@ data does not contain field1 @endif@
    @endsubfor@
    @endfor@
    */
    return genericTransform(loopContent, R"((?:@if@\s*(contains|equals|)\(?\$(\w+)\.*?(\w+)*\$(?:\)|,\"(.*)\"\)|)\s*((?:.*?\r?\n?)*)(?:@else@\s*((?:.*?\r?\n?)*))*@endif@)+)",
    [this, loopContentData, variableName](const QStringList & capturedText) -> QString {
        QString function = capturedText.value(1);
        QString conditionVarName = capturedText.value(2); // at(0) is the full captured text
        QString conditionVarField = capturedText.value(3);
        QString functionValue = capturedText.value(4);  // when function is "equals"
        QString ifStatement = capturedText.value(5);
        QString elseStatement = capturedText.value(6);

        if (conditionVarName == variableName) {
            // if there is no varField, this means the variable value (e.g. $tag$)
            // is encapsulated directly inside a custom Json object under the key "value"
            // else the value can be found normally in varField (e.g. $p.name$)
            // and conditionVarField is already set properly
            if (conditionVarField.isNull()) {
                conditionVarField = "value";
            }
            // add the line if some conditions are met:
            // if contains(..) then just check it is not null and valid
            bool isContainsFunction = (function == "contains");
            // if equals check that the value is equals to functionValue
            bool isEqualsFunction = (function == "equals") && (loopContentData[conditionVarField].toString() == functionValue);
            // other checks
            bool loopContentContainsField = loopContentData.contains(conditionVarField);
            bool isNotNullNorUndefined = !loopContentData[conditionVarField].isNull() && !loopContentData[conditionVarField].isUndefined();
            bool isBoolAndTrue = loopContentData[conditionVarField].isBool() && loopContentData[conditionVarField].toBool();
            bool isStringAndNotEqualsFalseNorEmptyNorNull = loopContentData[conditionVarField].isString() && loopContentData[conditionVarField].toString() != "false" && !loopContentData[conditionVarField].toString().isEmpty() && !loopContentData[conditionVarField].toString().isNull() && (function != "equals" || loopContentData[conditionVarField].toString() == functionValue);
            bool isDoubleAndNotZero = loopContentData[conditionVarField].isDouble() && loopContentData[conditionVarField].toDouble() != 0.0;
            bool isArrayAndNotEmpty = loopContentData[conditionVarField].isArray() && !loopContentData[conditionVarField].toArray().isEmpty() && function != "equals";
            bool isObjectAndNotEmpty = loopContentData[conditionVarField].isObject() && !loopContentData[conditionVarField].toObject().isEmpty() && function != "equals";

            if (loopContentContainsField && isNotNullNorUndefined
                    && (isBoolAndTrue
                        || isStringAndNotEqualsFalseNorEmptyNorNull
                        || isDoubleAndNotZero
                        || isArrayAndNotEmpty
                        || isObjectAndNotEmpty
                        || isContainsFunction
                        || isEqualsFunction)) {
                // append the content of the if statement
                return ifStatement;
            }
            else {
                // append the content of the else statement (if there is no @else@ this will be empty)
                return elseStatement;
            }
        }
        return QString();
    },
    true);
}

// -------------------- transformVariableInLoop --------------------
QString TransformEngine::transformVariableInLoop(const QString& loopContent, const QJsonObject& loopContentData, const QString& variableName) {
    // https://regex101.com/  \$\s*(\w+)\.*?(\w+)*\$
    return genericTransform(loopContent, R"(\$\s*(\w+)\.*?(\w+)*\$)" /* "\\$\\s*(\\w+)\\.*?(\\w+)*\\s*\\$"*/,
    [this, loopContentData, variableName](const QStringList & capturedText) -> QString {
        QString varName = capturedText.value(1);
        QString varField = capturedText.value(2);

        if (varName == variableName) {
            // if there is no varField, this means the variable value (e.g. $tag$)
            // is encapsulated directly inside a custom Json object under the key "value"
            // else the value can be found normally in varField (e.g. $p.name$)
            // and conditionVarField is already set properly
            if (varField.isNull()) {
                varField = "value";
            }

            // compute the value
            return valueOf(loopContentData, varField).toString();

        }
        else {
            return QString();
        }
    });
}

// -------------------- transformFunctionsInLoop --------------------
QString TransformEngine::transformFunctionsInLoop(const QString& loopContent, const QJsonObject& loopContentData, const QString& variableName) {
    // https://regex101.com/  \$\s*(\w+)\((\w+)\.*?(\w+)*\)\s*\$
    return genericTransform(loopContent, R"(\$\s*(\w+)\((\w+)\.*?(\w+)*\)\s*\$)"/* "\\$\\s*(\\w+)\\((\\w+)\\.*?(\\w+)*\\)\\s*\\$"*/,
    [this, loopContentData, variableName](const QStringList & capturedText) -> QString {
        QString function = capturedText.value(1);
        QString parameter = capturedText.value(2);
        QString parameterField = capturedText.value(3);

        if (parameter == variableName) {
            // if there is no varField, this means the variable value (e.g. $tag$)
            // is encapsulated directly inside a custom Json object under the key "value"
            // else the value can be found normally in varField (e.g. $p.name$)
            // and conditionVarField is already set properly
            if (parameterField.isNull()) {
                parameterField = "value";
            }

            return evaluateFunction(function, loopContentData, parameterField, QString());
        }

        return QString();
    });
}

// -------------------- transformLoop --------------------
QString TransformEngine::transformLoopInLoop(const QString& textToTransform, const QJsonObject& data, const QString& variableName) {
    return genericTransform(textToTransform, R"((?:@subfor@\s*\$(\w+)\$\s*in\s*\$(\w+)\.*?(\w+)*\$((?:.*?\r?\n?)*)@endsubfor@)+)" /*(?:@for@\\s*\\$(\\w+)\\$\\s*in\\s*\\$(\\w+)\\.*?(\\w+)*\\$((?:.*?\\r?\\n?)*)@endfor@)+*/,
    [this, data, variableName](const QStringList & capturedText) -> QString {
        QString result;

        // get the captured texts
        // @for@ loopVar in loopList.loopListKey    eg. @for@ $enumVal$ in $p.enumValues$
        //     loopContent
        // @endfor@
        //
        // Note: loopListKey is mandatory in loop inside loop
        QString loopVar = capturedText.value(1);     // enumVal
        QString loopList = capturedText.value(2);    // p
        QString loopListKey = capturedText.value(3); // enumValues
        QString loopContent = capturedText.value(4); // the text between @for@ ... in ... and @endfor@

        if (loopList == variableName) {

            // if the data contains loopList item
            if (data.contains(loopListKey)) {
                QJsonArray loopData;
                // the array must be inside the current data and must be an array
                if (data[loopListKey].isArray()) {
                    loopData = data[loopListKey].toArray();
                }

                // loop over the given JSON array
                for (indexInSubLoop = 0; indexInSubLoop < loopData.size(); indexInSubLoop++) {
                    QJsonObject loopItemObj;
                    QJsonValue loopItem = loopData.at(indexInSubLoop);

                    // if loop item contains an object, just use this
                    if (loopItem.isObject()) {
                        loopItemObj = loopItem.toObject();
                    }
                    else {
                        // encapsulate the JSon value (array, bool, etc...) in a dedicated JSon Object
                        loopItemObj["value"] = loopItem;
                    }

                    QString renderedContent = loopContent;
                    // transform conditions
                    renderedContent = transformConditionInLoop(renderedContent, loopItemObj, loopVar);
                    // transform variables
                    renderedContent = transformVariableInLoop(renderedContent, loopItemObj, loopVar);
                    // transform functions
                    renderedContent = transformFunctionsInLoop(renderedContent, loopItemObj, loopVar);

                    // add to result
                    result.append(renderedContent);
                }
                indexInSubLoop = -1;
            }
        }
        return result;
    },
    true);
}

// -------------------- transformLoop --------------------
QString TransformEngine::transformLoop(const QString& textToTransform, const QJsonObject& data) {
    // for loops https://regex101.com/r/EVNY50/1 (?:@for@\s*\$(\w+)\$\s*in\s*\$(\w+)\.*?(\w+)*\$((?:.*?\r?\n?)*)@endfor@)+
    return genericTransform(textToTransform, R"((?:@for@\s*\$(\w+)\$\s*in\s*\$(\w+)\.*?(\w+)*\$((?:.*?\r?\n?)*)@endfor@)+)" /*(?:@for@\\s*\\$(\\w+)\\$\\s*in\\s*\\$(\\w+)\\.*?(\\w+)*\\$((?:.*?\\r?\\n?)*)@endfor@)+*/,
    [this, data](const QStringList & capturedText) -> QString {
        QString result;

        // get the captured texts
        // @for@ loopVar in loopList.loopListKey
        //     loopContent
        // @endfor@
        //
        // Note: loopListKey is optional (e.g. in @for@ $tag$ in $classification.tags$ ... @endfor@)
        QString loopVar = capturedText.value(1);     // tag
        QString loopList = capturedText.value(2);    // classification
        QString loopListKey = capturedText.value(3); // tags
        QString loopContent = capturedText.value(4); // the text between @for@ ... in ... and @endfor@

        // if the data contains loopList item
        if (data.contains(loopList)) {
            QJsonArray loopData;
            // if loopListKey is null, the array is directly there (eg. @for@ $p$ in $parameters$)
            if (loopListKey.isNull()) {
                if (data[loopList].isArray()) {
                    loopData = data[loopList].toArray();
                }
            }
            else {
                // else the array is inside the loopList (eg. @for@ $tag$ in $classification.tags$)
                // Therefore loopList should be an object
                QJsonObject loopListObj = data[loopList].toObject();
                // and loopListKey array should be an array
                if (loopListObj.contains(loopListKey) && loopListObj[loopListKey].isArray()) {
                    loopData = loopListObj[loopListKey].toArray();
                }
            }

            // loop over the given JSON array
            for (indexInLoop = 0; indexInLoop < loopData.size(); indexInLoop++) {
                QJsonObject loopItemObj;
                QJsonValue loopItem = loopData.at(indexInLoop);

                // if loop item contains an object, just use this
                if (loopItem.isObject()) {
                    loopItemObj = loopItem.toObject();
                }
                else {
                    // encapsulate the JSon value (array, bool, etc...) in a dedicated JSon Object
                    loopItemObj["value"] = loopItem;
                }

                QString renderedContent = loopContent;
                // transform functions
                renderedContent = transformFunctionsInLoop(renderedContent, loopItemObj, loopVar);
                // transform loops inside loops
                renderedContent = transformLoopInLoop(renderedContent, loopItemObj, loopVar);
                // transform conditions
                renderedContent = transformConditionInLoop(renderedContent, loopItemObj, loopVar);
                // transform variables
                renderedContent = transformVariableInLoop(renderedContent, loopItemObj, loopVar);

                // add to result
                result.append(renderedContent);
            }
            indexInLoop = -1;
        }
        return result;
    },
    true);

}


// -------------------- transformCondition --------------------
QString TransformEngine::transformCondition(const QString& textToTransform, const QJsonObject& data) {
    return genericTransform(textToTransform, R"((?:@if@\s*(contains|equals|)\(?\$(\w+)\.*?(\w+)*\$(?:\)|,\"(.*)\"\)|)\s*((?:.*?\r?\n?)*)(?:@else@\s*((?:.*?\r?\n?)*))*@endif@)+)",
    [this, data](const QStringList & capturedText) -> QString {
        QString function = capturedText.value(1); // "contains", "equals", or empty
        QString conditionVarName = capturedText.value(2); // name of the tested variable
        QString conditionVarField = capturedText.value(3); // name of the variable field e.g. subfield for "var.subfield"
        QString functionValue = capturedText.value(4); // when function is "equals", this is the value to compare to
        QString ifStatement = capturedText.value(5); // the statement if the condition is true
        QString elseStatement = capturedText.value(6); // the statement if the condition is false

        // Get the value of the condition variable
        QVariant value = valueOf(data, conditionVarName, conditionVarField);

        // add the line if conditions are met:
        // if contains(..) then just check it is not null and valid
        bool isContainsFunction = (function == "contains");
        // if equals check that the value is equals to functionValue
        bool isEqualsFunction = (function == "equals") && (value.toString() == functionValue);
        // other checks
        bool isNotNullAndValid = !value.isNull() && value.isValid();
        bool isEvaluatedAsTrue = false;
        switch (value.userType()) {
            case QMetaType::Bool:
                isEvaluatedAsTrue = value.toBool();
                break;
            case QMetaType::QString:
                isEvaluatedAsTrue = value.toString() != "false" && !value.toString().isEmpty() && !value.toString().isNull() && (function != "equals" || value.toString() == functionValue);
                break;
            case QMetaType::Double:
            case QMetaType::Int:
                isEvaluatedAsTrue = value.toDouble() != 0.0;
                break;
            case QMetaType::QVariantList:
                isEvaluatedAsTrue = !value.toList().isEmpty() && function != "equals";
                break;
            case QMetaType::QVariantMap:
                isEvaluatedAsTrue = !value.toMap().isEmpty() && function != "equals";
                break;
        }
        if (isNotNullAndValid &&
                (isEvaluatedAsTrue
                 || isContainsFunction
                 || isEqualsFunction)) {
            return ifStatement; // content of the if statement
        }
        else {
            return elseStatement; // content of the else statement
        }
    });

}

// -------------------- transformVariable --------------------
QString TransformEngine::transformVariable(const QString& textToTransform, const QJsonObject& data) {
    // \$\s*(\w+)\.*?(\w+)*\$
    return genericTransform(textToTransform, "\\$\\s*(\\w+)\\.*?(\\w+)*\\$",
    [this, data](const QStringList & capturedText) -> QString {
        // replace variable value
        QString key = capturedText.value(1);
        QString subKey = capturedText.value(2);
        return valueOf(data, key, subKey).toString();
    });
}

// -------------------- transformFunctions --------------------
QString TransformEngine::transformFunctions(const QString& textToTransform, const QJsonObject& data) {
    // https://regex101.com/r/ \$\s*(\w+)\((\w+)*\)\s*\$
    return genericTransform(textToTransform, "\\$\\s*(\\w+)\\((\\w+)*\\)\\s*\\$",
    [this, data](const QStringList & capturedText) -> QString {
        QString function = capturedText.value(1);
        QString parameter = capturedText.value(2);

        return evaluateFunction(function, data, parameter, QString());
    });
}


// -------------------- valueOf --------------------
QVariant TransformEngine::valueOf(const QJsonObject& data, const QString& key, const QString& subkey) {
    // check the key exists
    if (data.contains(key)) {
        QJsonObject varObject;
        QString actualSubkey;
        if (subkey.isNull()) {
            // the value can be found directly inside data
            varObject = data;
            actualSubkey = key;
        }
        else {
            // The value is in the QJson object data["key"]
            varObject = data[key].toObject();
            actualSubkey = subkey;
        }

        // if the value exist, then return it as a variant
        if (varObject.contains(actualSubkey)) {
            QVariant value = varObject[actualSubkey].toVariant();
            if (value.userType() == QMetaType::Double) {
                return varObject[actualSubkey].toDouble();
            }
            else {
                return value;
            }
        }

    }
    return QVariant(); // invalid qvariant
}


// -------------------- substring --------------------
QString TransformEngine::substring(const QString& str, int startIndex, int endIndex) {
    QString substring = str.mid(startIndex, endIndex - startIndex);
    QString trimmedStr = str.trimmed();
    // filter out empty lines
    if (trimmedStr.isEmpty()) {
        return "";
    }
    else {
        return substring; //substring.prepend(QString("[←%1 ").arg(count)).append(QString(" %1→]").arg(count));
    }

}

// -------------------- extractLeadingSpaces --------------------
QString TransformEngine::extractLeadingSpaces(const QString& str) {
    QStringList lines = str.split("\n", Qt::SkipEmptyParts);
    if (lines.isEmpty()) {
        return QString("");    // Return empty string if input is empty
    }

    QString lastLine = lines.last();
    int startIndex = 0;
    while (startIndex < lastLine.size() && lastLine.at(startIndex).isSpace()) {
        startIndex++;
    }
    return lastLine.mid(0, startIndex);
}

// -------------------- removeTrailingSpaces --------------------
QString TransformEngine::removeTrailingSpaces(const QString& str) {
    int endIndex = str.size() - 1;
    while (endIndex >= 0 && str.at(endIndex).isSpace()) {
        endIndex--;
    }
    return str.left(endIndex + 1);
}

// -------------------- titleCase --------------------
QString TransformEngine::titleCase(const QString& str, const QString& separator) {
    QStringList words = str.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
    QStringList capitalizedWords;
    for (const QString& word : words) {
        if (!word.left(1).isUpper()) {
            capitalizedWords << word.left(1).toUpper() + word.mid(1).toLower();
        }
        else {
            // word starts with an upper case letter, just copy it (e.g. ITK → ITK, CamiTK → CamiTK)
            capitalizedWords << word;
        }
    }
    return capitalizedWords.join(separator);
}


// -------------------- clean --------------------
QString TransformEngine::clean(const QString& str) {
    // Remove non-alphanumeric characters except underscores
    QString cleaned = str;
    cleaned.replace(QRegularExpression("[^\\w\\s]"), "");
    return cleaned;
}

// -------------------- camelCase --------------------
QString TransformEngine::camelCase(const QString& str, const QString& separator) {
    // same as titleCase but first letter is lower case and all space between words are removed
    QString camelCaseStr = titleCase(clean(str), separator);
    if (camelCaseStr.isEmpty()) {
        return "";
    }
    else {
        camelCaseStr[0] = camelCaseStr[0].toLower();
        return camelCaseStr;
    }
}

// -------------------- append --------------------
QString TransformEngine::appendWithoutTrailingSpace(QStringList& result, const QString& str, int startIndex, int endIndex) {
    return append(result, str, startIndex, endIndex, true);
}

QString TransformEngine::append(QStringList& result, const QString& str, int startIndex, int endIndex, bool removeTrailingSpace) {
    QString strToAppend;
    if (startIndex != -1) {
        strToAppend = substring(str, startIndex, endIndex);
    }
    else {
        strToAppend = str;
    }

    QString toAppend = ((removeTrailingSpace) ? removeTrailingSpaces(strToAppend) : strToAppend);
    result.append(toAppend);
    return toAppend;
}

// -------------------- evaluateFunction --------------------
QString TransformEngine::evaluateFunction(const QString& functionName, const QJsonObject& data, const QString& parameter, const QString& parameterField) {

    if (functionName == "date") {
        return currentDate.toString("yyyy-MM-dd");
    }
    else if (functionName == "year") {
        return currentDate.toString("yyyy");
    }
    else if (functionName == "month") {
        return currentDate.toString("M");
    }
    else if (functionName == "day") {
        return currentDate.toString("d");
    }
    else if (functionName == "time") {
        return currentTime.toString("HH:mm:ss");
    }
    else if (functionName == "index") {
        return QString::number(indexInLoop);
    }
    else if (functionName == "title") {
        return titleCase(valueOf(data, parameter, parameterField).toString(), " ");
    }
    else if (functionName == "lowerCamelCase") {
        return camelCase(valueOf(data, parameter, parameterField).toString());
    }
    else if (functionName == "upperCamelCase") {
        return titleCase(clean(valueOf(data, parameter, parameterField).toString()));
    }
    else if (functionName == "lowerSnakeCase") {
        return titleCase(clean(valueOf(data, parameter, parameterField).toString()), "_").toLower();
    }
    else if (functionName == "upperSnakeCase") {
        return titleCase(clean(valueOf(data, parameter, parameterField).toString()), "_").toUpper();
    }
    else if (functionName == "kebabCase") {
        return titleCase(clean(valueOf(data, parameter, parameterField).toString()), "-").toLower();
    }
    else if (functionName == "joinKebabCase") {
        return titleCase(clean(valueOf(data, parameter, parameterField).toString())).toLower();
    }
    else {
        std::cerr << "Error: unknown template function " << functionName.toStdString();
        return QString();
    }
}

