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

#include "CamiTKExtensionModel.h"
#include "TransformEngine.h" // to check the expected extension names

#include <QStandardPaths>
#include <QDateTime>
#include <QCoreApplication>
#include <QMetaEnum>
#include <iostream>

// -------------------- constructor --------------------
CMakeProjectManager::CMakeProjectManager(const QString& camitkFilePath, QObject* parent) : QObject(parent) {
    this->camitkFilePath = camitkFilePath;

    // prepare process
    currentProcess = new QProcess(this);
    currentProcess->setWorkingDirectory(qApp->applicationDirPath());
    connect(currentProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
            this, &CMakeProjectManager::processFinished);

    // prepare default stages
    stages = { Check_System, Generate_Source_Files, Configure_CMake, Build_Project, Run_CamiTK_Config, Check_Integration, Cleanup};
    currentStageIndex = 0;
    status = true;

    // set build directory and remove if it exists
    QString sourcePath = QFileInfo(camitkFilePath).absolutePath();
    sourceDir.setPath(sourcePath);
    buildDir.setPath(sourcePath + "/build");
    status = status && buildDir.removeRecursively();

    // current executable bin dir (this should be a camitk application from inside the installed/build directory)
    camitkBinDir.setPath(QCoreApplication::applicationDirPath());

    isDebug = false;
}

// -------------------- destructor --------------------
CMakeProjectManager::~CMakeProjectManager() {
    delete currentProcess;
}

// -------------------- setStages --------------------
void CMakeProjectManager::setStages(QList<CMakeProjectManagerStage> stagesToPerform) {
    stages = stagesToPerform;
}

// -------------------- getStageName --------------------
QString CMakeProjectManager::getStageName(CMakeProjectManagerStage value) const {
    // Use QMetaObject to get the meta-object of this class
    const QMetaObject* metaObject = this->metaObject();

    // Find the meta-enum by name
    int index = metaObject->indexOfEnumerator("CMakeProjectManagerStage");
    QMetaEnum metaEnum = metaObject->enumerator(index);

    // Get the string representation of the enum value
    return QString(metaEnum.valueToKey(value)).replace("_", " ");
}

// -------------------- start --------------------
void CMakeProjectManager::start() {
    executeNextStage();
}

// -------------------- success --------------------
bool CMakeProjectManager::success() const {
    return status;
}

// -------------------- getCurrentStage --------------------
QString CMakeProjectManager::getCurrentStage() const {
    return getStageName(stages[currentStageIndex]);
}

// -------------------- getStages --------------------
QStringList CMakeProjectManager::getStages() const {
    QStringList stageNames;
    for (auto e : stages) {
        stageNames.append(getStageName(e));
    }
    return stageNames;
}

// -------------------- processFinished --------------------
void CMakeProjectManager::processFinished(int exitCode, QProcess::ExitStatus exitStatus) {
    status = (exitStatus == QProcess::NormalExit && exitCode == 0);

    QString message;
    QString output = currentProcess->readAllStandardOutput();
    if (output.size() > 0) {
        message += QString("Standard Output:\n%1").arg(output);
    }
    output = currentProcess->readAllStandardError();
    if (output.size() > 0) {
        message += QString("\nStandard Error:%1\n").arg(output);
    }

    emit stageFinished(getCurrentStage(), status, message);

    QCoreApplication::processEvents(); // Ensure the event loop processes events

    if (status) {
        currentStageIndex++;
        executeNextStage();
    }
}

// -------------------- executeNextStage --------------------
void CMakeProjectManager::executeNextStage() {
    if (currentStageIndex >= stages.size()) {
        emit allStagesFinished(status);
        return;
    }

    emit stageStarted(getCurrentStage());

    QCoreApplication::processEvents(); // Ensure the event loop processes events

    switch (stages[currentStageIndex]) {
        case Check_System:
            checkSystem();
            break;
        case Generate_Source_Files:
            generateSourceFiles();
            break;
        case Configure_CMake:
            configureCMake();
            break;
        case Build_Project:
            buildProject();
            break;
        case Run_CamiTK_Config:
            runCamiTKConfig();
            break;
        case Check_Integration:
            checkIntegration();
            break;
        case Cleanup:
            cleanup();
            break;
    }

}

// -------------------- checkSystem --------------------
void CMakeProjectManager::checkSystem() {
    QStringList output;

    output << "- CamiTK file: " + camitkFilePath;
    output << "- Source directory: " + sourceDir.absolutePath();
    output << "- CamiTK bin directory: " + camitkBinDir.absolutePath();

#if defined(_WIN32)
    // Check if the executable name ends with "-debug"
    isDebug = QFileInfo(QCoreApplication::applicationFilePath()).fileName().endsWith("-debug")
              || QFileInfo(QCoreApplication::applicationFilePath()).fileName().endsWith("-debug.exe");
    if (!isDebug) {
        // try harder by looking into current CamiTK dir executable
        QFile camitkConfigRelease(camitkBinDir.absoluteFilePath(QString("camitk-config.exe")));
        QFile camitkConfigDebug(camitkBinDir.absoluteFilePath(QString("camitk-config-debug.exe")));
        isDebug = camitkConfigDebug.exists() && !camitkConfigRelease.exists();
    }
#endif
    output << "- Has debug suffix: " + QString((isDebug) ? "yes" : "no");

    //-- cmake
    QProcess checkSystemCommand;
    checkSystemCommand.setWorkingDirectory(qApp->applicationDirPath());
    checkSystemCommand.setProgram("cmake");
    checkSystemCommand.setArguments({ "--version" });
    checkSystemCommand.setProcessChannelMode(QProcess::MergedChannels);
    checkSystemCommand.start();
    if (checkSystemCommand.waitForFinished(-1)) {
        QRegularExpression cmakeVersionRegExp(R"(cmake version ([\d\.]*))", QRegularExpression::MultilineOption);
        QRegularExpressionMatch match = cmakeVersionRegExp.match(checkSystemCommand.readAll());
        if (match.hasMatch()) {
            output << "- Found CMake " + match.captured(1);
        }
    }
    else {
        output << QString(checkSystemCommand.readAll());
        emit stageFinished(getCurrentStage(), false, QString("Could not found CMake.\n%1\nError:\n%2\nOutput:\n%3\n").arg("cmake --version").arg(checkSystemCommand.errorString()).arg(output.join("\n")));
        return;
    }

    //-- camitk-extensiongenerator
    QString camitkExtensionGenerator = camitkBinDir.absoluteFilePath(QString("camitk-extensiongenerator%1").arg((isDebug) ? "-debug" : ""));
    checkSystemCommand.setProgram(camitkExtensionGenerator);
    checkSystemCommand.setArguments({ "--version" });
    checkSystemCommand.setProcessChannelMode(QProcess::MergedChannels);
    checkSystemCommand.start();
    if (checkSystemCommand.waitForFinished(-1)) {
        QRegularExpression camitkVersionRegExp(R"(^Version:\sCamiTK\s([\d\w\-\.]*))", QRegularExpression::MultilineOption);
        QRegularExpressionMatch match = camitkVersionRegExp.match(checkSystemCommand.readAll());
        if (match.hasMatch()) {
            output << "- Found camitk-extensiongenerator " + match.captured(1);
        }
    }
    else {
        output << QString(checkSystemCommand.readAll());
        emit stageFinished(getCurrentStage(), false, QString("Could not found CamiTK extension generator.\n%1 --version\n Error:\n%2\nOutput:\n%3\n").arg(camitkExtensionGenerator).arg(checkSystemCommand.errorString()).arg(output.join("\n")));
        return;
    }

    //-- camitk-config
    QString camitkConfig = camitkBinDir.absoluteFilePath(QString("camitk-config%1").arg((isDebug) ? "-debug" : ""));
    checkSystemCommand.setProgram(camitkConfig);
    checkSystemCommand.setArguments({ "--version" });
    checkSystemCommand.setWorkingDirectory(qApp->applicationDirPath());
    checkSystemCommand.setProcessChannelMode(QProcess::MergedChannels);
    checkSystemCommand.start();
    if (checkSystemCommand.waitForFinished(-1)) {
        QRegularExpression camitkVersionRegExp(R"(using CamiTK\s([\d\w\-\.]*))", QRegularExpression::MultilineOption);
        QRegularExpressionMatch match = camitkVersionRegExp.match(checkSystemCommand.readAll());
        if (match.hasMatch()) {
            output << "- Found camitk-config " + match.captured(1);
        }
    }
    else {
        output << QString(checkSystemCommand.readAll());
        emit stageFinished(getCurrentStage(), false, QString("Could not found CamiTK config.\n%1 --version\nError:\n%2\nOutput:\n%3\n").arg(camitkConfig).arg(checkSystemCommand.errorString()).arg(output.join("\n")));
        return;
    }

    emit stageFinished(getCurrentStage(), true, output.join("\n") + "\n[OK] System checked successfully.");
    currentStageIndex++;
    executeNextStage();
}

// -------------------- generateSourceFiles --------------------
void CMakeProjectManager::generateSourceFiles() {
    QString camitkExtensionGenerator = camitkBinDir.absoluteFilePath(QString("camitk-extensiongenerator%1").arg((isDebug) ? "-debug" : ""));

    currentProcess->setProgram(camitkExtensionGenerator);
    QStringList arguments;
    arguments << "--file" << camitkFilePath
              << "--directory" << sourceDir.path();
    currentProcess->setArguments(arguments);

    currentProcess->start();
    currentProcess->waitForFinished(-1);
}

// -------------------- configureCMake --------------------
void CMakeProjectManager::configureCMake() {
    if (!QDir().mkpath(buildDir.absolutePath())) {
        emit stageFinished(getCurrentStage(), false, QString("Failed to create build directory \"%1\"\n").arg(buildDir.absolutePath()));
        return;
    }
    std::cout << "- Build directory created: " + buildDir.absolutePath().toStdString();

    QStringList arguments;
    arguments << "-S" << sourceDir.path()
              << "-B" << buildDir.path();

    currentProcess->setProgram("cmake");
    currentProcess->setArguments(arguments);
    currentProcess->start();
    currentProcess->waitForFinished(-1);
}

// -------------------- buildProject --------------------
void CMakeProjectManager::buildProject() {
    QStringList arguments;
    arguments << "--build" << buildDir.path();
    arguments << "--parallel";

#ifdef Q_OS_WIN
    // add debug/release config
    arguments << "--config" << ((isDebug) ? "Debug" : "Release");
#endif

    currentProcess->setProgram("cmake");
    currentProcess->setArguments(arguments);

    currentProcess->start();
    currentProcess->waitForFinished(-1);
}

// -------------------- runCamiTKConfig --------------------
void CMakeProjectManager::runCamiTKConfig() {
    QString camitkConfig = camitkBinDir.absoluteFilePath(QString("camitk-config%1").arg((isDebug) ? "-debug" : ""));
    currentProcess->setProgram(camitkConfig);
    currentProcess->setArguments({"--config"});
    // this stage is to check that everything went smooth for C++ extension → build directory must exist
    currentProcess->setWorkingDirectory(buildDir.absolutePath());
    currentProcess->setStandardOutputFile(buildDir.filePath("config-output.txt"));

    currentProcess->start();
    currentProcess->waitForFinished(-1);
}

// -------------------- checkIntegration --------------------
void CMakeProjectManager::checkIntegration() {
    // read from output file
    QFile camitkConfigOutputFile(buildDir.filePath("config-output.txt"));
    if (!camitkConfigOutputFile.open(QFile::ReadOnly | QFile::Text)) {
        emit stageFinished(getCurrentStage(), false, QString("Failed to open camitk-config output from file \"%1\"\nerror: %2").arg(buildDir.filePath("config-output.txt")).arg(camitkConfigOutputFile.errorString()));
        return;
    }
    QTextStream in(&camitkConfigOutputFile);
    QString camitkConfigOutput = in.readAll();

    camitkConfigOutputFile.close();

    // get CamiTK file content (what is expected)
    CamiTKExtensionModel camitkFile(camitkFilePath);
    VariantDataModel& data = camitkFile.getModel();
    QMap<QString, QString> expectedActionExtensions;

    TransformEngine transformEngine;
    QString extensionName = transformEngine.transformToString("$title(name)$", QJsonObject::fromVariantMap(data.getValue().toMap()));
    expectedActionExtensions.insert(extensionName, QString::number(data["actions"].size()));

    // check what was actually integrated
    QStringList output;

    // Note: for component extension use: R"(- \[W\]\s([^\.]*)\.* (\w*)$)" where the last captured is the file extension managed by the component extension
    QRegularExpression workingDirExtensionsRegExp(R"(- \[W\]\s([^\.]*)\.* (\d*)\s(\w*)$)", QRegularExpression::MultilineOption);
    QRegularExpressionMatch match = workingDirExtensionsRegExp.match(camitkConfigOutput);
    int extensionIndex = 0;
    while (match.hasMatch()) {
        QString extensionName = match.captured(1);
        QString numberOfClasses = match.captured(2);
        QString extensionType = match.captured(3);

        // check for matches
        if (expectedActionExtensions.contains(extensionName)) {
            output.append(QString("[OK] Found expected extension \"%1\"").arg(extensionName));
            if (expectedActionExtensions.value(extensionName) == numberOfClasses) {
                output.append(QString("[OK] Found expected %2 %3").arg(numberOfClasses).arg(extensionType));
            }
            else {
                output.append(QString("[FAILED] Unexpected number of %1: found %2 but expected %3").arg(extensionType).arg(numberOfClasses).arg(expectedActionExtensions.value(extensionName)));
                status = false;
            }
            expectedActionExtensions.remove(extensionName);
        }
        else {
            output.append(QString("- Found unexpected %1: %2 %3").arg(extensionName).arg(numberOfClasses).arg(extensionType));
            status = false;
        }

        // get next
        extensionIndex = match.capturedEnd();
        match = workingDirExtensionsRegExp.match(camitkConfigOutput, extensionIndex);
    }
    // Check if all expected were found
    if (expectedActionExtensions.empty()) {
        output.append("[OK] all expected extensions found");
    }
    else {
        for (auto& missing : expectedActionExtensions.keys()) {
            output.append(QString("[FAILED] missing %1").arg(missing));
        }
        status = false;
    }

    if (status) {
        emit stageFinished(getCurrentStage(), true, output.join("\n"));
        currentStageIndex++;
        executeNextStage();
    }
    else {
        output.append(QString("camitk-config output:\n%1").arg(camitkConfigOutput));
        emit stageFinished(getCurrentStage(), false, output.join("\n"));
        return;
    }
}

// -------------------- cleanup --------------------
void CMakeProjectManager::cleanup() {
    emit stageFinished("Cleanup", status, QString("Build directory: %1").arg(buildDir.absolutePath()));
    if (status) {
        currentStageIndex++;
        executeNextStage();
    }
}