/*****************************************************************************
 * $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$
 ****************************************************************************/
#ifndef __TRANSFORM_ENGINE__
#define __TRANSFORM_ENGINE__

#include <QJsonObject>

/**
 * @brief Utility class to transform strings depending on json object values
 *
 * This is inspired by inja but is much much much more basic.
 *
 * The template string can contains loops, conditions and variable fields.
 *
 * ### Example
 *
 * Example using `templateStr`
 ```bash
// code for $name$ (class $upperCamelCase(name)$) or $upperSnakeCase(name)$
// generated on $date()$ $time()$
@for@ $param$ in $parameters$
   param = addParameter(new Property("$param.name$", $param.defaultValue$, tr("$param.description$"), "$param.unit$"));
   @if@ $param.readOnly$
       param->setReadOnly(true);
   @else@
       // param is not read only
   @endif@
   @subfor@ $ev$ in $param.enumValues$
       // value: $ev$
   @endsubfor@
@endfor@
@if@ $name$
    // given name: $name$
@else@
    // no given name
@endif@
@if@ $option$
    // option: $option$
@else@
    // no option
@endif@
@if@ $optionList$
    // optionList is given
    @for@ $o$ in $optionList$
    // option: $o$
    @endfor@
@else@
    // no optionlist
@endif@
```
 *
 * and data the given json data:
```json
 {
    "name": "test data",
    "optionList": [
        "option1",
        "option2",
        false,
        -5,
        0
    ],
    "parameters": [
        {
            "defaultValue": 100,
            "description": "Testing double",
            "name": "testDouble",
            "readOnly": false,
            "type": "double",
            "unit": ""
        },
        {
            "defaultValue": "QVector3D(0.0, 0.0, 0.0)",
            "description": "test 3D vector",
            "name": "testVector3D",
            "readOnly": true,
            "type": "QVector3D",
            "unit": "mm"
        },
        {
            "defaultValue": 1,
            "description": "test enum",
            "enumValues": [
                "V1",
                "V2",
                "V3"
            ],
            "name": "testenum",
            "readOnly": false,
            "type": "enum",
            "unit": "mm"
        }
    ]
}
```
 *
 * then calling:
 *
 * `transformToString(templateStr, data)`
 *
 * will produce:
```cpp
// code for test data (class TestData) or TEST_DATA
// generated on 2024-05-16 18:25:08

   param = addParameter(new Property("testDouble", 100.0, tr("Testing double"), ""));
   // param is not read only
   param = addParameter(new Property("testVector3D", QVector3D(0.0, 0.0, 0.0), tr("test 3D vector"), "mm"));
   param->setReadOnly(true);
   param = addParameter(new Property("testenum", 1.0, tr("test enum"), "mm"));
   // param is not read only

       // value: V1
       // value: V2
       // value: V3
// given name: test data

// no option

// optionList is given

    // option: option1
    // option: option2
    // option: false
    // option: -5.0
    // option: 0.0

```
 *
 * ### Functions
 *
 * The following utility functions can be used in the template file (see example table below):
 * - $date()$ → generation date as YYYY-MM-DD
 * - $year()$, $month()$, $day()$ → respectively current year (as YYYY), month (1 to 12) and day (1 to 31)
 * - $time()$ → generation time as HH:MM:SS
 * - $index(p)$ → index of the current p in a loop (-1 if the template engine is outside a loop)
 * - $title(var)$ → clean var (remove everything that is not space nor letters) and capitalize the first letter of every words
 * - $lowerCamelCase(var)$ → same as titleCase but remove non-alphanumeric characters except underscores, first letter is lowercase and all space between words are removed
 * - $upperCamelCase(var)$ → same as camelCase but first letter is uppercase
 * - $lowerSnakeCase(var)$ → same as titleCase but remove non-alphanumeric characters except underscores, all space between words are replaced by an underscore and all letters are lowercase
 * - $upperSnakeCase(var)$ → same as lowerSnakeCase but all letters are uppercase
 * - $kebabCase(var)$ → same as lowerCamelCase but all letters are lowercase and words are separated by "-"
 * - $joinKebabCase(var)$ → same as kebabCase but all words are joined (no separator)
 *
 *  \example
 *
 * | Input | Function | Output |
 * |-------|----------|--------|
 * | "my class   name! 123 (test)"   | title           | "My Class Name! 123 (test)"  |
 * | "my class   name! 123 (test)"   | lowerCamelCase  | "myClassName123Test"         |
 * | "my class   name! 123 (test)"   | upperCamelCase  | "MyClassName123Test"         |
 * | "my class   name! 123 (test)"   | lowerSnakeCase  | "my_class_name_123_test"     |
 * | "my class   name! 123 (test)"   | upperSnakeCase  | "MY_CLASS_NAME_123_TEST"     |
 * | "my class   name! 123 (test)"   | kebabCase       | "my-class-name-123-test"     |
 * | "my class   name! 123 (test)"   | joinKebabCase   | "myclassname123test"         |
 *
 * ### for statement
 *
 * Used for array to loop over each value
```bash
@for@ $i$ in $array$
  ... // here $i takes the value of a[i]
  ... // $i.j$ corresponds to a[i].j
@endfor@
```
 * Loop inside loop are supported using `@subfor@..@endsubfor@`
 *
 * ### if statement
 *
 * In if statement you can use the contains and equals as well (this can also be used in loops):
 * - `@if@ contains($field$) ... @endif@` → check if data contains field
 * - `@if@ contains($object.field$) ... @endif@` → check if data contains object that contains field
 * - `@if@ equals($a$,"yes") yes @else@ no @endif@` → check if the value of data a is equals to "yes"
 * - `@for@ $i$ in $a$ @if@ equals($i.b$,"foo") $index(i)$ has b==foo ($i.b$), @else@ $index(i)$ does not have b==foo, @endif@ @endfor@` → check if each value of i in a are equals to "foo"
 * - `@if@ $parameter$ ...@endif@` → check if parameter can be evaluated to true, i.e.:
 *    - parameter is present in the data
 *    - and it is not null nor of undefined value
 *    - and is either
 *        - a boolean and equals to true
 *        - a string but not empty, nor null nor equals to "false"
 *        - a double but not equals to 0.0
 *        - an int but not equals to 0
 *        - an array, a map or an object but not empty
 *
 * ### Algorithm
 *
 * The main algorithm has three steps
 * 1. replace loop
 *    1.1 replace functions inside loop
 *    1.2 replace loop inside loop
 *    1.3 replace conditions inside loop
 *    1.4 replace variables inside loop
 * 2. replace conditions
 * 3. replace variables
 * 4. replace functions
 *
 */

class TransformEngine {

public:
    /// Constructor
    /// set up the date and time
    TransformEngine();

    /// Use this constructor for test purpose only (to fix the date and be able to compare output)
    TransformEngine(QDate currentDate, QTime currentTime);

    /// set the current template string
    bool setTemplateString(QString templateString);

    /// @brief transform the current template string using the given data and save it to filename
    /// @param data QJsonObject that contains the data used to transform the template
    /// @param filename the filename to use
    /// @param overwrite if false, the file is not overwritten if it already exists (default: true)
    /// @return true if the file was generated
    ///
    /// \note filename can contain a variable (e.g. "/tmp/$name$.cpp" will use data["name"] to determine the actual filename)
    //  or a function (e.g. "/tmp/$upperCamelCase(name)$.h" will use upperCamelCase(data[name]) to determine the actual filename)
    bool transformToFile(const QJsonObject& data, const QString& filename, bool overwrite = true);

    /// transform the given template using the given data to a string.
    /// You can use this method as a utility function it does not modify the
    /// class attributes apart from temporary loop counters
    QString transformToString(const QString& templateString, const QJsonObject& data);

private:
    /// the template as read from the current template file (only set once, never modified)
    QString templateString;

    /// system date and time of the engine instantiation
    QDate currentDate;
    QTime currentTime;

    /// index of the current item in the current `@for@` loop (-1 if outside a loop)
    int indexInLoop;

    /// index of the current item in the current `@subfor@` loop (-1 if outside a loop)
    int indexInSubLoop;

    /// @name global transformation for statement
    /// @{
    /// Check for `@for@ $var$ in $list$` inside textToTransform and replace the corresponding variable values
    /// and test condition (`@if@`) depending on the values defined inside the list
    QString transformLoop(const QString& textToTransform, const QJsonObject& data);

    /// Check for `@if@ $var$` inside textToTransform and leave or remove the condition statement body
    /// depending on data[var]:
    /// - if data[var] is not present, remove the condition body
    /// - else if data[var] is a bool, remove the condition body if the value is false
    /// - else leave the condition body
    QString transformCondition(const QString& textToTransform, const QJsonObject& data);
    /// @}

    /// Transform all variables $var$ in textToTransform by their corresponding values
    /// (if they are found) in data
    QString transformVariable(const QString& textToTransform, const QJsonObject& data);

    /// Transform supported functions to values
    /// (see class description for the current list of supported utility functions)
    QString transformFunctions(const QString& textToTransform, const QJsonObject& data);

    /// @name in loop transformation
    /// transform methods that are called from inside a loop body. The loopContent must be
    /// the text inside a `@for@` statement:
    /// ```bash
    /// @for@ loopVar in loopList.loopListKey
    ///     loopContent
    /// @endfor@
    /// ```

    /// @{
    /// Transforms `@if@...@endif@` inside the given loopContent text.
    /// Each `@if@...@endif@` inside loopContent is checked to see if it matches variableName.
    /// If $var$s in `@if@ $var$` is equals to variableName and if the value of loopContentData[var]
    /// is a boolean of value true, the condition body (string between `@if@...@endif@`) is kept in the returned
    /// string, otherwise it is removed from it.
    ///
    /// Example:
    /// If loopContent is :
    /// \code {.cpp}
    ///    line1
    ///    @if@ $myVar$
    ///       line2
    ///    @endif@
    /// \endcode
    ///
    /// Then the returned string will be:
    /// \code {.cpp}
    ///    line1
    ///    line2
    /// \endcode
    /// if variableName is equals to "myVar" and loopContentData["myVar"] exists and is equals to true
    ///
    /// or
    /// \code {.cpp}
    ///    line1
    /// \endcode
    /// if variableName is not "myVar", or loopContentData["myVar"] does not exist, or it is not a boolean or it is false
    ///
    /// \note that var can also contains a field (e.g. $var.field$) in this case the method
    /// check the value of loopContent[var][field] instead.
    QString transformConditionInLoop(const QString& loopContent, const QJsonObject& loopContentData, const QString& variableName);

    /// Replace all $var$ by the value of loopContentData[var] if $var$ is equals to variableName.
    /// \note that var can also contains a field (e.g. $var.field$) in this case the method
    /// use the value of loopContent[var][field] instead.
    QString transformVariableInLoop(const QString& loopContent, const QJsonObject& loopContentData, const QString& variableName);

    /// Transform supported functions to values in a loop
    /// (see class description for the current list of supported utility functions)
    QString transformFunctionsInLoop(const QString& loopContent, const QJsonObject& loopContentData, const QString& variableName);

    /// Transform a loop inside another
    QString transformLoopInLoop(const QString& loopContent, const QJsonObject& loopContentData, const QString& variableName);
    /// @}

    /// @name utility methods
    /// @{
    /// generic transform: check regularExpression inside textToTransform for all matching, append the value returned by
    /// matchFunction(capture1, capture2, capture3) and finally append the end of the text.
    /// The operation can be done with or without removing trailing space.
    QString genericTransform(const QString& textToTransform, const QString& regularExpression, const std::function<QString(const QStringList&)>& matchFunction, bool withoutTrailingSpace = false);

    /// return non-empty string extracted from str from startIndex to endIndex
    /// if endIndex is -1, it will get all the characters up to the end of str
    QString substring(const QString& str, int startIndex, int endIndex = -1);

    /// useful to get the indentation spaces from a string
    QString extractLeadingSpaces(const QString& str);

    /// useful to remove the empty/newline string
    QString removeTrailingSpaces(const QString& str);

    /// transform str to title case (each word starts with an uppercase character) and combine all words using the separator
    QString titleCase(const QString& str, const QString& separator = "");

    /// Remove non-alphanumeric characters except underscores and split the string by spaces
    QString clean(const QString& str);

    /// remove all characters that are not letters or digit and transform the remaining sentence to camelCase (remove spaces between words)
    QString camelCase(const QString& str, const QString& separator = "");

    /// returns the QVariant value of data[key] if subkey is the null string of data[key][subkey] otherwise
    /// returns a null string if data does not contains the key/subkey
    QVariant valueOf(const QJsonObject& data, const QString& key, const QString& subkey = QString());

    /// append str (or part of str if startIndex / endIndex are provided), removing trailing spaces
    /// @return what was added
    QString append(QStringList& result, const QString& str, int startIndex = -1, int endIndex = -1, bool removeTrailingSpace = false);
    QString appendWithoutTrailingSpace(QStringList& result, const QString& str, int startIndex = -1, int endIndex = -1);

    /// save content to filename, show an error and return false if
    /// - overwrite is false and the file already exists
    /// - or the file cannot be opened in write mode
    bool saveToFile(const QString& content, const QString& filename, bool overwrite = true);

    /// evaluate the given function using valueOf(data,parameter,parameterField)
    QString evaluateFunction(const QString& functionName, const QJsonObject& data, const QString& parameter, const QString& parameterField);
    /// @}

};

#endif // __TRANSFORM_ENGINE__