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

#include <QCoreApplication>
#include <QJsonParseError>
#include <QJsonArray>

#include <iostream>

// -------------------- constructor --------------------
ExtensionGenerator::ExtensionGenerator(const QString& camitkFilePath, const QString& outputDirectoryName) {
    success = true;
    this->camitkFilePath = camitkFilePath;
    success = camitkExtensionModel.load(camitkFilePath);

    success = success && createOutputDirectoryIfNeeded(outputDirectoryName + QDir::separator());
}

// -------------------- generate --------------------
bool ExtensionGenerator::generate() {
    if (!success) {
        // Don't start as something wrong happened before
        std::cerr << "ExtensionGenerator: setup incomplete, cannot start template transformation." << std::endl;
        return false;
    }

    //-- 1. clear output
    statusMessage.clear();
    warningMessage.clear();

    //-- 2. update model using general/overall information
    updateExtensionModel();

    //-- 3. get the updated model, path and json objects
    VariantDataModel& extensionModel = camitkExtensionModel.getModel();
    QFileInfo camitkFileInfo(camitkFilePath);
    QJsonObject extensionJSON = QJsonObject::fromVariantMap(extensionModel.getValue().toMap());

    //-- 4. generation in top level source directory (systematic check)
    statusMessage.append(QString("Generating %1 extension source code...").arg(isCpp() ? "C++" : "Python"));
    statusMessage.append("- source CamiTK file: " + camitkFileInfo.absoluteFilePath());
    statusMessage.append("- output directory: " + outputDir.absolutePath());

    //-- check if this extension is already part of a CEP, i.e., the root directory contains a CMakeLists.txt
    QDir camitkFileDirectory(camitkFileInfo.absolutePath());
    QDir camitkFileDirectoryRoot(camitkFileDirectory);
    camitkFileDirectoryRoot.cdUp();
    if (!QFile(camitkFileDirectoryRoot.filePath("CMakeLists.txt")).exists()) {
        //-- copy useful .gitignore and .vscode/launch.json if they don't already exist
        if (!QFile(camitkFileDirectory.filePath(".gitignore")).exists()) {
            QFile::copy(":/gitignore", camitkFileDirectory.filePath(".gitignore"));
            statusMessage.append("- Copying .gitignore...");
        }
        else {
            statusMessage.append("- .gitignore already exists: not overwritten");
        }

        QDir vsCodeDir = camitkFileDirectory;
        vsCodeDir.mkdir(".vscode");
        vsCodeDir.cd(".vscode");
        transformEngine.setTemplateString(fileToString(":/vscode-launch.json"));
        if (transformEngine.transformToFile(extensionJSON, vsCodeDir.filePath("launch.json"), false)) {
            statusMessage.append("- Generating .vscode/launch.json...");
        }
        else {
            statusMessage.append("- .vscode/launch.json already exists: not overwritten");
        }

        //-- add top directory FindCamiTK.txt if standalone CEP C++ project
        if (isCpp()) {
            statusMessage.append("- Generating standalone CamiTK Extension Project...");
            if (!QFile(camitkFileDirectory.filePath("FindCamiTK.cmake")).exists()) {
                QFile::copy(":/FindCamiTK.cmake", camitkFileDirectory.filePath("FindCamiTK.cmake"));
                statusMessage.append("- Copying FindCamiTK.cmake...");
            }
            else {
                statusMessage.append("- FindCamiTK.cmake already exists: not overwritten");
            }
        }
    }
    else {
        statusMessage.append("- Generating extension inside another CEP...");
    }

    if (isCpp()) {
        //-- Top level CMakeLists  if it does not exist
        transformEngine.setTemplateString(fileToString(":/CMakeLists.txt.in"));
        if (transformEngine.transformToFile(extensionJSON, camitkFileDirectory.filePath("CMakeLists.txt"), false)) {
            statusMessage.append("- Generating top-level CMakeLists.txt...");
        }
        else {
            statusMessage.append("- CMakeLists.txt already exists: not overwritten");
        }
    }

    //-- 5. Action Extension and Actions
    //   generate user defined action source code for each action if they do not exist
    generateActionExtension();

    //-- 6. Print out accumulated status and error
    std::cout << statusMessage.join("\n").toStdString() << std::endl;
    std::cout << warningMessage.join("\n").toStdString() << std::endl;

    return true;
}

// -------------------- createOutputDirectoryIfNeeded --------------------
bool ExtensionGenerator::createOutputDirectoryIfNeeded(const QString& dirPath) {
    QDir dir(dirPath);
    if (!dir.isAbsolute()) {
        QString pwd = QDir::currentPath();
        dir.setPath(pwd + QDir::separator() + dirPath);
    }

    if (!dir.exists()) {
        if (!dir.mkpath(".")) {
            std::cerr << "ExtensionGenerator warning: failed to create directory " << dir.absolutePath().toStdString();
            return false;
        }
    }

    outputDir.setPath(dir.absolutePath());
    return true;
}

// -------------------- fileToString --------------------
QString ExtensionGenerator::fileToString(const QString& filename) {
    QFile file(filename);
    QString fileContent;
    if (!file.open(QFile::ReadOnly | QFile::Text)) {
        std::cerr << "ExtensionGenerator warning: cannot open template engine file \"" << filename.toStdString() << "\"\nerror: " << file.errorString().toStdString();
    }

    QTextStream in(&file);
    fileContent = in.readAll();

    file.close();

    return fileContent;
}

// -------------------- updateExtensionModel --------------------
void ExtensionGenerator::updateExtensionModel() {
    //-- get the path to the .camitk file
    QFileInfo camitkFileInfo(camitkFilePath);

    //-- get the model containing all the extensions
    VariantDataModel& extensionModel = camitkExtensionModel.getModel();

    // create JSON object from the model
    QJsonObject extensionJSON = QJsonObject::fromVariantMap(extensionModel.getValue().toMap());

    //-- add camitk file information
    extensionModel["camitkFile"] = camitkFileInfo.fileName();
    extensionModel["camitkFileAbsolutePath"] = camitkFileInfo.absoluteFilePath();

    //-- add camitk installation information
    // precondition: this is ran by either camitk-imp or camitk-extensiongenerator but both
    // are installed in the same directory → this determines CamiTK DIR bin directory
    // current executable bin dir (this should be a camitk application from inside the installed/build directory)
    // This will set EXTENSION_GENERATOR_CAMITK_DIR in the extension top-level CMakeLists.txt
    // which will be used to find camitk-config
    QDir camitkBinDir;
    camitkBinDir.setPath(QCoreApplication::applicationDirPath());
    QDir camiTKDir = camitkBinDir;
    camiTKDir.cdUp();
    extensionModel["camitkDir"] = camiTKDir.absolutePath();

    //-- add cmake module path relatively to current executable path
    // This will set EXTENSION_GENERATOR_CMAKE_MODULE_PATH in the extension top-level CMakeLists.txt
    // which should work in most cases (i.e., if CamitK is either installed locally or globally or
    // even if this is used during the SDK build, during local build or CI build) and will locate
    // the CamiTK CMake modules and macros required to build the extension.
    QDir camiTKCMakeModuleDir = camiTKDir;
    camiTKCMakeModuleDir.cd("share");
    camiTKCMakeModuleDir.cd(CAMITK_SHORT_VERSION); // CAMITK_SHORT_VERSION is defined in CMakeLists.txt
    camiTKCMakeModuleDir.cd("cmake");
    extensionModel["camitkCMakeModulePath"] = QString("%1;%2").arg(camiTKCMakeModuleDir.absolutePath()).arg(camiTKCMakeModuleDir.filePath("macros"));

    //-- add license text information
    QString licenseString = extensionModel["license"].toString();
    if (licenseString == "BSD") {
        extensionModel["licenseComment"] = transformEngine.transformToString(fileToString(":/licenses/BSD"), extensionJSON);
    }
    else if (licenseString == "GPL" || licenseString == "GPL-3") {
        extensionModel["licenseComment"] = transformEngine.transformToString(fileToString(":/licenses/GPL-3"), extensionJSON);
    }
    else if (licenseString == "LGPL" || licenseString == "LGPL-3") {
        extensionModel["licenseComment"] = transformEngine.transformToString(fileToString(":/licenses/LGPL-3"), extensionJSON);
    }
    else if (licenseString == "LGPL CamiTK" || licenseString == "LGPL-3 CamiTK") {
        // replace license markers with themselves
        extensionModel["CAMITK_LICENCE_BEGIN"] = "$CAMITK_LICENCE_BEGIN$";
        extensionModel["CAMITK_LICENCE_END"] = "$CAMITK_LICENCE_END$";
        extensionModel["licenseComment"] = transformEngine.transformToString(fileToString(":/licenses/LGPL-3 CamiTK"), extensionJSON);
    }
    else {
        // add custom license into a multiline comment if it is not a comment itself.
        QRegularExpression multiLineCommentRegEx(R"(^\/\*\s*((?:.*?\r?\n?)*)\*\/)");
        if (multiLineCommentRegEx.match(licenseString).hasMatch()) {
            extensionModel["licenseComment"] = licenseString;
        }
        else {
            extensionModel["licenseComment"] = "/*\n" + licenseString + "\n*/\n";
        }
    }

    //-- All action/component/viewer/library must inherits all the updated information
    inheritFromExtension("actions");
    // inheritFromExtension("components");
    // inheritFromExtension("viewers");
    // inheritFromExtension("libraries");
}

// -------------------- inheritFromExtension --------------------
void ExtensionGenerator::inheritFromExtension(const QString type) {
    //-- get the model containing all the extensions
    VariantDataModel& extensionModel = camitkExtensionModel.getModel();
    VariantDataModel& extensionArray = extensionModel[type];
    if (extensionArray.isValid()) {
        for (auto& ext : extensionArray) {
            ext["CAMITK_LICENCE_BEGIN"] = "$CAMITK_LICENCE_BEGIN$";
            ext["CAMITK_LICENCE_END"] = "$CAMITK_LICENCE_END$";
            ext["licenseComment"] = extensionModel["licenseComment"];
            ext["extensionDependencies"] = extensionModel["extensionDependencies"];
        }
    }
}

// -------------------- generateActionExtension --------------------
void ExtensionGenerator::generateActionExtension() {
    //-- get the model containing all the extensions
    VariantDataModel& extensionModel = camitkExtensionModel.getModel();

    if (extensionModel.getValue().toMap().size() == 0) {
        warningMessage.append("Cannot generate extension from empty model.");
        return;
    }

    if (isCpp()) {
        // There are two types of generation for C++: Standard or HotPlug (default)
        QString generationType = extensionModel["generationType"].toString(); // either Standard or HotPlug
        if (generationType.isEmpty() || generationType.isNull()) {
            // default
            generationType = "HotPlug";
        }

        statusMessage.append("Generating " + generationType + " C++ Action Extension...");
        if (generationType == "HotPlug") {
            generateCppHotPlugActionExtension();
        }
        else {
            /// For Standard C++ extension, check if the extension generator is called
            /// - to generate the user action source code (UserAction.cpp)
            /// - or to generate in-build (hidden) files as well (ActionExtension and Action source),
            ///   so called wrapper source code as it wraps the user code using generated
            ///   CamiTK Action and ActionExtension code
            ///
            /// To distinguishes the two situation, this method just compare the given output directory and
            /// the camitkFile directory. If they are different then the (hidden) in-build non-user action
            /// source files (ActionExtension.h ActionExtension.cpp, Action.h and Action.cpp) have to be generated.
            if (QFileInfo(camitkFilePath).absolutePath() == outputDir.absolutePath()) {
                // generate the CMake files and UserAction (if needed)
                generateCppStandardUserActionExtension();
            }
            else {
                // generate the ActionExtension class header and implementation files,
                // as well as the action implementation and header files for all actions
                generateCppStandardWrapperActionExtension();
            }
        }
    }
    else {
        statusMessage.append("Generating Python Action Extension...");
        generatePythonActionExtension();
    }
}

// -------------------- generateCppHotPlugActionExtension --------------------
void ExtensionGenerator::generateCppHotPlugActionExtension() {
    // create the actions directory
    QFileInfo camitkFileInfo(camitkFilePath);
    QDir actionDirectory(camitkFileInfo.absolutePath());
    actionDirectory.mkdir("actions");
    actionDirectory.cd("actions");

    //-- HotPlug generation
    QFile actionLibrariesCMakeFile(actionDirectory.filePath("CMakeLists.txt"));
    // empty string (if this is the first generation) or current content of action CMakeLists.txt
    QString cMakeFileContent;
    // CMake instructions to add to the action CMakeLists (may be empty if all actions are already present)
    QStringList cmakeAppendStream;

    // 1. Get the CMakeLists.txt content to check if the action is not already present (see below)
    // can only be done if the action CMakeLists.txt already exists and is readable
    if (actionLibrariesCMakeFile.exists()) {
        if (!actionLibrariesCMakeFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
            warningMessage.append(QString("Cannot open existing action CMakeLists.txt for checking: %1").arg(actionLibrariesCMakeFile.errorString()));
        }
        else {
            QTextStream cMakeFileContentStream(&actionLibrariesCMakeFile);
            cMakeFileContent = cMakeFileContentStream.readAll();
            actionLibrariesCMakeFile.close();
        }
    }

    // get the model containing all the extensions
    VariantDataModel& extensionModel = camitkExtensionModel.getModel();

    // 2. For each action: create CMake macros and initial source code if needed
    // model → JSON (needed by the transform engine)
    QJsonArray actionsArrayJSON = QJsonArray::fromVariantList(extensionModel["actions"].getValue().toList());
    for (const QJsonValueRef& actionJSONValue : actionsArrayJSON) {
        // create required information from the action name
        QJsonObject actionJSON = actionJSONValue.toObject();
        QString libName = transformEngine.transformToString("$joinKebabCase(name)$", actionJSON).toLower();
        QString className = transformEngine.transformToString("$upperCamelCase(name)$", actionJSON);

        // 2.1 Generate the camitk_library macro if not already present
        if (!cMakeFileContent.contains("LIBNAME " + libName)) {
            statusMessage.append("- Adding " + libName + " in action CMakeLists.txt");

            cmakeAppendStream << "# -- Create targets for action " + libName + "\n";
            cmakeAppendStream << transformEngine.transformToString(fileToString(":/HotPlug/ActionCMakeLists.txt.in"), actionJSON) << "\n\n";
        }
        else {
            statusMessage.append("- " + libName + " already in action CMakeLists.txt, nothing to do.");
        }

        // 2.2 Generate user-defined code from the template if it does not exist
        transformEngine.setTemplateString(fileToString(":/HotPlug/UserActionLib.cpp.in"));
        if (transformEngine.transformToFile(actionJSON, actionDirectory.filePath(className + ".cpp"), false)) {
            statusMessage.append("- Generating initial user source code for action \"" + actionJSON["name"].toString() + "\" (" + className + ".cpp)...");
        }
        else {
            statusMessage.append("- User source code for action \"" + actionJSON["name"].toString() + "\" already exists (" + className + ".cpp): not overwritten");
        }
    }

    // 3. Append CMake instructions to the ActionLibraries.cmake
    if (!actionLibrariesCMakeFile.open(QIODevice::Append | QIODevice::Text)) {
        warningMessage.append(QString("Cannot append to the action CMakeLists.txt for writing: %1").arg(actionLibrariesCMakeFile.errorString()));
    }
    else {
        QTextStream out(&actionLibrariesCMakeFile);
        out << cmakeAppendStream.join("\n");

        // all camitk_library are now defined in ActionLibraries.cmake
        actionLibrariesCMakeFile.close();
    }
}

// -------------------- generateCppStandardUserActionExtension --------------------
void ExtensionGenerator::generateCppStandardUserActionExtension() {
    // create the actions directory
    QFileInfo camitkFileInfo(camitkFilePath);
    QDir actionDirectory(camitkFileInfo.absolutePath());
    actionDirectory.mkdir("actions");
    actionDirectory.cd("actions");

    // get the model containing all the extensions
    VariantDataModel& extensionModel = camitkExtensionModel.getModel();

    //-- 1. generate action CMakeLists.txt if not already there
    // model → JSON (needed by the transform engine)
    QJsonObject extensionJSON = QJsonObject::fromVariantMap(extensionModel.getValue().toMap());
    transformEngine.setTemplateString(fileToString(":/Standard/ActionCMakeLists.txt.in"));
    if (transformEngine.transformToFile(extensionJSON, actionDirectory.filePath("CMakeLists.txt"), false)) {
        statusMessage.append("- Generating CMakeLists.txt for actions...");
    }
    else {
        statusMessage.append("- CMakeLists.txt for action already exists: not overwritten");
    }

    //-- 2. For each action, generate user action implementation if not already there
    // model → JSON (needed by the transform engine)
    QJsonArray actionsArrayJSON = QJsonArray::fromVariantList(extensionModel["actions"].getValue().toList());
    transformEngine.setTemplateString(fileToString(":/Standard/UserAction.cpp.in"));
    for (const auto& actionJSONValue : actionsArrayJSON) {
        QJsonObject actionJSON = actionJSONValue.toObject();
        QString className = transformEngine.transformToString("$upperCamelCase(name)$", actionJSON);

        // generate user-defined code from the template if it does not exist
        if (transformEngine.transformToFile(actionJSON, actionDirectory.filePath(className + ".cpp"), false)) {
            statusMessage.append("- Generating initial user source code for action \"" + actionJSON["name"].toString() + "\" (" + className + ".cpp)...");
        }
        else {
            statusMessage.append("- User source code for action \"" + actionJSON["name"].toString() + "\" already exists (" + className + ".cpp): not overwritten");
        }
    }
}

// -------------------- generateCppStandardWrapperActionExtension --------------------
void ExtensionGenerator::generateCppStandardWrapperActionExtension() {
    // get the model containing all the extensions
    VariantDataModel& extensionModel = camitkExtensionModel.getModel();

    // This code is only executed when called from the camitk_extension_generator CMake macro
    // (see CamiTKExtensionGeneration.cmake)
    // outputDir should be equals to "${CMAKE_CURRENT_BINARY_DIR}/generated"

    //-- 1. generate the ActionExtension header and implementation wrapper
    // model → JSON (needed by the transform engine)
    QJsonObject extensionJSON = QJsonObject::fromVariantMap(extensionModel.getValue().toMap());
    statusMessage.append("- Generating wrapper source code for action extension \"" + extensionModel["name"].toString() + "\" source code (overwriting)...");
    transformEngine.setTemplateString(fileToString(":/Standard/ActionExtension.h.in"));
    transformEngine.transformToFile(extensionJSON, outputDir.filePath("$upperCamelCase(name)$ActionExtension.h"));
    transformEngine.setTemplateString(fileToString(":/Standard/ActionExtension.cpp.in"));
    transformEngine.transformToFile(extensionJSON, outputDir.filePath("$upperCamelCase(name)$ActionExtension.cpp"));

    //-- 2. generate the Action header and implementation wrapper for each action
    // model → JSON (needed by the transform engine)
    QJsonArray actionsArrayJSON = QJsonArray::fromVariantList(extensionModel["actions"].getValue().toList());
    transformEngine.setTemplateString(fileToString(":/Standard/Action.cpp.in"));
    for (const auto& actionJSON : actionsArrayJSON) {
        statusMessage.append("- Generating wrapper source code for action \"" + actionJSON.toObject()["name"].toString() + "\" (overwriting)...");
        transformEngine.transformToFile(actionJSON.toObject(), outputDir.filePath("$upperCamelCase(name)$.cpp"));
    }

    transformEngine.setTemplateString(fileToString(":/Standard/Action.h.in"));
    for (const auto& actionJSON : actionsArrayJSON) {
        transformEngine.transformToFile(actionJSON.toObject(), outputDir.filePath("$upperCamelCase(name)$.h"));
    }
}

// -------------------- generatePythonActionExtension --------------------
void ExtensionGenerator::generatePythonActionExtension() {
    // Copy the stub file to the typings/ directory (for IDE support)
    QFileInfo camitkFileInfo(camitkFilePath);
    QDir typingsDirectory(camitkFileInfo.absolutePath());
    typingsDirectory.mkdir("typings");
    typingsDirectory.cd("typings");
    if (!QFile(typingsDirectory.filePath("camitk.pyi")).exists()) {
        QFile::copy(":/Python/camitk.pyi", typingsDirectory.filePath("camitk.pyi"));
        statusMessage.append("- Copying stub file camitk.pyi...");
    }
    else {
        statusMessage.append("- stub file already exists: not overwritten");
    }

    // get the model containing all the extensions
    VariantDataModel& extensionModel = camitkExtensionModel.getModel();

    // For each action, generate user action python script if not already there
    // model → JSON (needed by the transform engine)
    QJsonArray actionsArrayJSON = QJsonArray::fromVariantList(extensionModel["actions"].getValue().toList());
    transformEngine.setTemplateString(fileToString(":/Python/action_python.py.in"));
    for (const auto& actionJSONValue : actionsArrayJSON) {
        QJsonObject actionJSON = actionJSONValue.toObject();
        QString pythonName = transformEngine.transformToString("$lowerSnakeCase(name)$", actionJSON);

        // generate user-defined code from the template if it does not exist
        if (transformEngine.transformToFile(actionJSON, outputDir.absoluteFilePath(pythonName + ".py"), false)) {
            statusMessage.append("- Generating initial python script for action \"" + actionJSON["name"].toString() + "\" (" + pythonName + ".py)...");
        }
        else {
            statusMessage.append("- Python script for action \"" + actionJSON["name"].toString() + "\" already exists (" + pythonName + ".py): not overwritten");
        }
    }
}

// -------------------- isCpp --------------------
bool ExtensionGenerator::isCpp() {
    // get the model containing all the extensions
    VariantDataModel& extensionModel = camitkExtensionModel.getModel();

    // can be empty (default = C++), "C++" or "Python"
    QString language = extensionModel["language"].toString();
    return (language.isEmpty() || language.isNull() || language != "Python");
}
