/*****************************************************************************
 * $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$
 ****************************************************************************/

// -- Core stuff
#include "Core.h"
#include "CamiTKVersion.h"
#include "ExtensionManager.h"
#include "Action.h"
#include "Property.h"
#include "Viewer.h"
#include "PropertyObject.h"
#include "Application.h"
#include "AbortException.h"
#include "PythonManager.h"
#include "Log.h"

// -- QT stuff
#include <QDir>
#include <QApplication>
#include <QProcess>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonArray>
#include <QMetaProperty>
#include <QMetaType>
#include <QVector3D>

#include <vtkVersion.h>


// MSVC does not have the C++ Standard rint function (it should be included in any C99 implementation)
#if defined(_WIN32) && !defined(__MINGW32__) && (_MSC_VER < 1800)
#include <math.h>
double rint(double x) {
    // middle value point test
    if (ceil(x + 0.5) == floor(x + 0.5)) {
        int a = (int)ceil(x);
        if (a % 2 == 0) {
            return ceil(x);
        }
        else {
            return floor(x);
        }
    }
    else {
        return floor(x + 0.5);
    }
}
#endif

namespace camitk {

// ------------- getPathsAsJson (static) -----------------
const QJsonObject Core::getPathsAsJson() {
    QString compileType;
    if (isDebugBuild()) {
        compileType = "DEBUG";
    }
    else {
        compileType = "RELEASE";
    }

    QString stringOS;
#ifdef _WIN32
    stringOS = "WIN32";
#endif
#ifdef __APPLE__
    stringOS = "APPLE";
#endif
#ifdef __linux__
    stringOS = "LINUX";
#endif

    // for simplification reason, if global dir is equals to cwd, show it
    QString globalDir = Core::getGlobalInstallDir();
    QString userDir = Core::getUserInstallDir();
    QString currentWorkingDir = Core::getCurrentWorkingDir();
    if (globalDir == currentWorkingDir) {
        globalDir = "[not globally installed: using current working directory as repository]";
    }
    else if (globalDir == userDir) {
        globalDir = "[locally installed version: using user config directory as repository]";
    }

    // list of keys in display order
    QStringList informationKeys = getInformationPathKeys();

    //-- compile all informations
    QJsonObject camitkPaths;
    int keyId = 0;
    camitkPaths.insert(informationKeys[keyId++], QString(Core::version()));
    camitkPaths.insert(informationKeys[keyId++], QString(Core::shortVersion()));
    camitkPaths.insert(informationKeys[keyId++], QString(Core::soVersion()));
    camitkPaths.insert(informationKeys[keyId++], stringOS);
    camitkPaths.insert(informationKeys[keyId++], compileType);
    camitkPaths.insert(informationKeys[keyId++], QString(QT_VERSION_STR));
    camitkPaths.insert(informationKeys[keyId++], QString(vtkVersion::GetVTKVersion()));
    camitkPaths.insert(informationKeys[keyId++], QString(Core::getPythonStatus()).replace("\n", " "));
    camitkPaths.insert(informationKeys[keyId++], globalDir);
    camitkPaths.insert(informationKeys[keyId++], userDir);
    camitkPaths.insert(informationKeys[keyId++], currentWorkingDir);
    camitkPaths.insert(informationKeys[keyId++], Core::getTestDataDir());
    camitkPaths.insert(informationKeys[keyId++], QJsonArray::fromStringList(Core::getComponentDirectories()));
    camitkPaths.insert(informationKeys[keyId++], QJsonArray::fromStringList(Core::getActionDirectories()));
    camitkPaths.insert(informationKeys[keyId++], QJsonArray::fromStringList(Core::getViewerDirectories()));

    return camitkPaths;
}

// ------------- getPaths (static) -----------------
const QString Core::getPaths() {
    QJsonObject camitkPaths = getPathsAsJson();

    // list of keys in display order
    QStringList informationKeys = getInformationPathKeys();
    int maxKeyLength = std::max_element(informationKeys.begin(), informationKeys.end(), [](QString lhs, QString rhs) {
        return lhs.length() < rhs.length();
    })->length();
    maxKeyLength += 14; // add some spacing

    QStringList pathStringList;
    QString value;
    QString alignWithNextLine = QString("\n").leftJustified(maxKeyLength + 2, ' ');
    for (auto key : informationKeys) {
        if (camitkPaths[key].isArray()) {
            QStringList valueArray;
            for (auto val : camitkPaths[key].toArray()) {
                valueArray << val.toString();
            }
            value = valueArray.join(alignWithNextLine);
        }
        else {
            value = camitkPaths[key].toString();
        }
        pathStringList << QString("- %1").arg(key).leftJustified(maxKeyLength, '.') + " " + value;
    }
    return pathStringList.join("\n");
}

// ------------- getConfigAsJson (static) -----------------
const QJsonObject Core::getConfigAsJson() {
    QJsonObject config;
    config.insert("paths", getPathsAsJson());

    // get the global, user and current working directories
    QString globalDir = Core::getGlobalInstallDir();
    QString userDir = Core::getUserInstallDir();
    QString currentWorkingDir = Core::getCurrentWorkingDir();

    // global, local working dir and user component extensions
    QJsonArray componentExtensions;

    //-- all component extensions
    for (ComponentExtension* ce : ExtensionManager::getComponentExtensionsList() + ExtensionManager::getDataDirectoryComponentsList()) {
        QJsonObject extension;
        extension.insert("name", ce->getName());
        extension.insert("fileExtensions", QJsonArray::fromStringList(ce->getFileExtensions()));
        extension.insert("description", ce->getDescription());
        if (ce->hasDataDirectory()) {
            extension.insert("dataDirectory", true);
        }
        extension.insert("location", ExtensionManager::getInstallationString(ce->getLocation(), globalDir, userDir, currentWorkingDir)); // [G], [L], [W] or [U]

        componentExtensions.append(extension);
    }

    config.insert("componentExtensions", componentExtensions);

    //-- all action extensions
    // global, local working dir and user action extensions
    QJsonArray actionExtensions;

    for (ActionExtension* ae : ExtensionManager::getActionExtensionsList()) {
        QJsonObject extension;
        extension.insert("name", ae->getName());
        extension.insert("description", ae->getDescription());

        QJsonArray actionArray;
        for (Action* a : ae->getActions()) {
            QJsonObject action;

            action.insert("name", a->getName());
            action.insert("description", a->getDescription());

            QJsonObject classification;
            classification.insert("family", a->getFamily());
            classification.insert("tags", QJsonArray::fromStringList(a->getTag()));
            action.insert("classification", classification);
            action.insert("componentClass", a->getComponentClassName());

            // Cannot really call getWidget() as some actions don't check if getTargets()
            // is not empty, this can generate some segfault/crash
            // QWidget* aw = a->getWidget();
            // if (aw == nullptr) {
            //     action.insert("gui", "No GUI");
            // }
            // else {
            //     if (dynamic_cast<ActionWidget*>(aw) != nullptr) {
            //         action.insert("gui", "Default Action GUI");
            //     }
            //     else {
            //         action.insert("gui", "Custom GUI");
            //     }
            // }

            // introspect all parameters
            QJsonArray parameters;
            QByteArrayList dynamicPropertyNames = a->dynamicPropertyNames();
            for (int idx = 0; idx < dynamicPropertyNames.size(); ++idx) {
                parameters.append(Property::getPropertyInformation(a, QString(dynamicPropertyNames.at(idx))));
            }
            action.insert("parameters", parameters);
            actionArray.append(action);
        }
        extension.insert("actions", actionArray);
        extension.insert("location", ExtensionManager::getInstallationString(ae->getLocation(), globalDir, userDir, currentWorkingDir));
        actionExtensions.append(extension);
    }
    config.insert("actionExtensions", actionExtensions);

    //-- all viewer extensions
    // global, local working dir and user action extensions
    QJsonArray viewerExtensions;

    for (ViewerExtension* ve : ExtensionManager::getViewerExtensionsList()) {
        QJsonObject extension;
        extension.insert("name", ve->getName());
        extension.insert("description", ve->getDescription());

        QJsonArray viewerArray;
        for (Viewer* v : ve->getViewers()) {
            QJsonObject viewer;
            viewer.insert("name", v->getName());
            viewer.insert("description", v->getDescription());
            // viewer.insert("uuid", v->getUuid());
            viewer.insert("type", (v->getType() == Viewer::EMBEDDED) ? "EMBEDDED" : "DOCKED");
            if (v->getPropertyObject() != nullptr) {
                QJsonArray propertyArray;
                PropertyObject* vpo = v->getPropertyObject();
                for (int i = 0; i < vpo->getNumberOfProperties(); i++) {
                    QJsonObject property;
                    property.insert("name", vpo->getPropertyName(i));
                    Property* vp = vpo->getProperty(vpo->getPropertyName(i));
                    // Note that more introspection is possible on property, but that should be enough
                    QVariant::Type type = vp->getInitialValue().type();
                    const char* typeName = QMetaType::typeName(type);
                    property.insert("type", typeName);
                    property.insert("description", vp->getDescription());
                    propertyArray.append(property);
                }
                viewer.insert("properties", propertyArray);
                viewer.insert("componentClass", QJsonArray::fromStringList(v->getComponentClassNames()));
            }
            viewerArray.append(viewer);
        }
        extension.insert("viewers", viewerArray);
        extension.insert("location", ExtensionManager::getInstallationString(ve->getLocation(), globalDir, userDir, currentWorkingDir));
        viewerExtensions.append(extension);
    }
    config.insert("viewerExtensions", viewerExtensions);

    return config;
}

// ------------- getConfig (static) -----------------
const QString Core::getConfig() {
    QStringList diagnosis;

    //-- first get all paths
    diagnosis << getPaths();

    // how many extensions of a givent type (component or action) are available
    unsigned int extensionCount = 0;
    // how many file formats are managed (for component extension) or how many single actions are available (for action extension)
    unsigned int extensionUnitCount = 0;
    // how many extensions installed globally (on the machine/CAMITK_DIR directory)
    unsigned int globalCount = 0;
    // how many extensions installed locally in the user global installation (%APPDATA% on win, and ~/.config on Linux/Mac)
    unsigned int localCount = 0;
    // how many extensions installed in the current working directory (generally the build directory)
    unsigned int workingDirCount = 0;
    // how many extensions installed manually in the application (using another specific path)
    unsigned int userCount = 0;
    QString installationDirectory;

    // get the global, user and current working directories
    QString globalDir = Core::getGlobalInstallDir();
    QString userDir = Core::getUserInstallDir();
    QString currentWorkingDir = Core::getCurrentWorkingDir();

    //-- component extensions
    QStringList components;

    // regular component extensions
    const QList< ComponentExtension* >& allCE = ExtensionManager::getComponentExtensionsList();

    for (ComponentExtension* ce : allCE) {
        installationDirectory = ExtensionManager::getInstallationString(ce->getLocation(), globalDir, userDir, currentWorkingDir);

        components << "  - " + installationDirectory + " " + ce->getName().leftJustified(39, '.')  + " " + ce->getFileExtensions().join(", "); // to get more information, use ce->getDescription();
        extensionCount++;
        extensionUnitCount += ce->getFileExtensions().size();
        if (installationDirectory == "[G]") {
            globalCount++;
        }
        else if (installationDirectory == "[L]") {
            localCount++;
        }
        else if (installationDirectory == "[W]") {
            workingDirCount++;
        }
        else if (installationDirectory == "[U]") {
            userCount++;
        }
    }

    // directory extensions
    const QList< ComponentExtension* >& allDCE = ExtensionManager::getDataDirectoryComponentsList();
    for (ComponentExtension* ce : allDCE) {
        installationDirectory = ExtensionManager::getInstallationString(ce->getLocation(), globalDir, userDir, currentWorkingDir);

        components << "  - " + installationDirectory + " " + ce->getName().leftJustified(39, '.') + " directory";
        extensionCount++;
        extensionUnitCount += ce->getFileExtensions().size();
        if (installationDirectory == "[G]") {
            globalCount++;
        }
        else if (installationDirectory == "[L]") {
            localCount++;
        }
        else if (installationDirectory == "[W]") {
            workingDirCount++;
        }
        else if (installationDirectory == "[U]") {
            userCount++;
        }
    }

    diagnosis << "- Number of Component Extensions............... " + QString::number(extensionCount) + " (locations: " + QString::number(globalCount) + " global, " + QString::number(localCount) + " local, " + QString::number(workingDirCount) + " in working directory, " + QString::number(userCount) + " manually installed by user)";
    diagnosis << "- Number of File Extensions Supported.......... " + QString::number(extensionUnitCount);

    //-- action extensions
    extensionCount = extensionUnitCount = globalCount = localCount = workingDirCount = userCount = 0;

    QStringList actions;
    const QList< ActionExtension* >& allActions = ExtensionManager::getActionExtensionsList();

    for (ActionExtension* ae : allActions) {
        QStringList actionNames;
        for (Action* a : ae->getActions()) {
            actionNames << a->getName();
        }

        installationDirectory = ExtensionManager::getInstallationString(ae->getLocation(), globalDir, userDir, currentWorkingDir);

        actions << "  - " +  installationDirectory + " " + ae->getName().leftJustified(39, '.') + " " + QString::number(ae->getActions().size()) + " action" + ((ae->getActions().size() > 1) ? "s" : ""); // + ": " + actionNames.join(", ");
        extensionCount++;
        extensionUnitCount += ae->getActions().size();
        if (installationDirectory == "[G]") {
            globalCount++;
        }
        else if (installationDirectory == "[L]") {
            localCount++;
        }
        else if (installationDirectory == "[W]") {
            workingDirCount++;
        }
        else if (installationDirectory == "[U]") {
            userCount++;
        }
    }

    diagnosis << "- Number of Action Extensions.................. " + QString::number(extensionCount) + " (locations: " + QString::number(globalCount) + " global, " + QString::number(localCount) + " local, " + QString::number(workingDirCount) + " in working directory, " + QString::number(userCount) + " manually installed by user)";
    diagnosis << "- Number of Actions............................ " + QString::number(extensionUnitCount);

    //-- viewer extensions
    extensionCount = extensionUnitCount = globalCount = localCount = workingDirCount = userCount = 0;

    QStringList viewers;
    const QList< ViewerExtension* >& allViewers = ExtensionManager::getViewerExtensionsList();

    for (ViewerExtension* ve : allViewers) {
        QStringList viewerNames;
        for (Viewer* v : ve->getViewers()) {
            viewerNames << v->getName();
        }

        installationDirectory = ExtensionManager::getInstallationString(ve->getLocation(), globalDir, userDir, currentWorkingDir);

        viewers << "  - " +  installationDirectory + " " + ve->getName().leftJustified(39, '.') + " " + QString::number(ve->getViewers().size()) + " viewer" + ((ve->getViewers().size() > 1) ? "s" : ""); // + ": " + viewerNames.join(", ");
        extensionCount++;
        extensionUnitCount += ve->getViewers().size();
        if (installationDirectory == "[G]") {
            globalCount++;
        }
        else if (installationDirectory == "[L]") {
            localCount++;
        }
        else if (installationDirectory == "[W]") {
            workingDirCount++;
        }
        else if (installationDirectory == "[U]") {
            userCount++;
        }
    }

    diagnosis << "- Number of Viewer Extensions.................. " + QString::number(extensionCount) + " (locations: " + QString::number(globalCount) + " global, " + QString::number(localCount) + " local, " + QString::number(workingDirCount) + " in working directory, " + QString::number(userCount) + " manually installed by user)";
    diagnosis << "- Number of Viewers............................ " + QString::number(extensionUnitCount);

    //-- details of the extensions
    diagnosis << "- Registered component extensions:";
    diagnosis += components;

    diagnosis << "- Registered action extensions:";
    diagnosis += actions;

    diagnosis << "- Registered viewer extensions:";
    diagnosis += viewers;

    return diagnosis.join("\n");
}

// -------------------- getExtensionFilter --------------------
QStringList Core::getExtensionFilter() {
    QStringList pluginFilter;
    // linux: .so and macOs: .dylib
    pluginFilter << "*.so." + QString(Core::soVersion()) << "*." + QString(Core::soVersion()) + ".dylib";

    // windows case: .dll, but can have a debug postfix as well
    if (Core::isDebugBuild()) {
        pluginFilter << "*" + QString(Core::debugPostfix()) + ".dll";
    }
    else {
        pluginFilter << "*.dll";
    }

    return pluginFilter;
}

// ------------- getInstallDirectories (static) -----------------
const QStringList Core::getInstallDirectories(QString suffix, bool exitOnError) {
    QStringList dir;

    // find the build directory according to the current directory
    QDir currentWorkingDir(getCurrentWorkingDir());
    if (currentWorkingDir.cd(suffix)) {
        dir.append(currentWorkingDir.canonicalPath().toUtf8());
    }

    // user actions
    QDir userConfigDir(getUserInstallDir());
    if (userConfigDir.cd(suffix) && !dir.contains(userConfigDir.canonicalPath().toUtf8())) {
        dir.append(userConfigDir.canonicalPath().toUtf8());
    }

    // global actions
    QDir globalInstallDir(getGlobalInstallDir());
    if (globalInstallDir.cd(suffix) && !dir.contains(globalInstallDir.canonicalPath().toUtf8())) {
        dir.append(globalInstallDir.canonicalPath().toUtf8());
    }

    if (dir.empty() && exitOnError) {
        QString ext = QFileInfo(suffix).completeBaseName();
        QString msg = QObject::tr("Installation directory not found: no %1 extensions can be found. Exiting. If you did not install CamiTK, you should at least set your current working directory to CamiTK Community Edition build dir. Note: the current working directory is %2, looking for %3").arg(ext, getCurrentWorkingDir(), suffix);
        qDebug() << msg; // to force show on console
        CAMITK_ERROR_ALT(msg);
        throw AbortException(msg.toStdString());
    }

    return dir;
}

// ------------- getExtensionDirectories (static) -----------------
const QStringList Core::getExtensionDirectories(QString extensionType) {
    QStringList installDirectories = getInstallDirectories(QString(Core::libDir()) + "/" + QString(Core::shortVersion()) + "/" + extensionType);
    installDirectories.removeDuplicates();

    //QStringList multiarchInstalledDirectories(installDirectories.begin(), installDirectories.end());
    if (QString(Core::libDir()) != "lib") {
        // multiarch installation in "lib/xx" requires checking non-multiarch aware current repositories that uses default "lib/"
        QStringList supplementalInstallDirectories = getInstallDirectories("lib/" + QString(Core::shortVersion()) + "/" + extensionType, false);
        supplementalInstallDirectories.removeDuplicates();
        installDirectories += supplementalInstallDirectories;
    }

    return installDirectories;
}

// ------------- getInformationPathKeys (static) -----------------
const QStringList Core::getInformationPathKeys() {
    return { "CamiTK version", "CamiTK Short Version", "CamiTK SO NAME", "Operating System", "Build type", "QT Version", "VTK Version", "Python Status", "Global Installation Directory [G]", "Local Installation Directory [L]", "Current Working Directory [W]", "Test Data Directory", "Component Extension Directories", "Action Extension Directories", "Viewer Extension Directories"};
}

// ------------- getActionDirectories (static) -----------------
const QStringList Core::getActionDirectories() {
    return getExtensionDirectories("actions");
}

// ------------- getComponentDirectories (static) -----------------
const QStringList Core::getComponentDirectories() {
    return getExtensionDirectories("components");
}

// ------------- getViewerDirectories (static) -----------------
const QStringList Core::getViewerDirectories() {
    return getExtensionDirectories("viewers");
}

// ------------- getTestDataDir (static) -----------------
const QString Core::getTestDataDir() {
    // check the testdata installation directory (but do not exit if it does not exist)
    QStringList testDataDirectories = getInstallDirectories("share/" + QString(Core::shortVersion()) + "/testdata", false);
    // just returns the first one if exists
    if (testDataDirectories.size() > 0) {
        return testDataDirectories.at(0);
    }
    else
        // otherwise returns user home directory
    {
        return QDir::home().canonicalPath().toUtf8();
    }
}

// ------------- getGlobalInstallDir (static) -----------------
const QString Core::getGlobalInstallDir() {
    QDir camitkDir;
    QByteArray camitkDirectory;
    bool processOk = false;

    if (Application::getName() != "camitk-config") {
        // run camitk-config to get the "installed" directory
        // note: camitk-config must be in the path, or it is not considered as installed
        // (installed = available at any time)
        QProcess process;
        process.setWorkingDirectory(Application::applicationDirPath());
        process.start("camitk-config", QStringList() << "--camitk-dir", QIODevice::ReadWrite | QIODevice::Text);

        if (process.waitForStarted(-1)) {
            while (process.waitForReadyRead(-1)) {
                camitkDirectory += process.readAllStandardOutput();
            }
            camitkDir.setPath(QString(camitkDirectory).trimmed());
            // if the directory does not exist check CamiTKDir.txt
            processOk = camitkDir.exists();
        }
    }

    // if there was a problem or if this is camitk-config, check CamiTKDir.txt
    if (!processOk) {
        // not found use the last installation path if available
        QFile file(getUserInstallDir() + "/CamiTKDir.txt");
        if (file.exists() && file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            camitkDirectory = file.readLine();
            camitkDir.setPath(QString(camitkDirectory).trimmed());
        }
        else {
            // everything failed, use current application directory path (last chance)
            camitkDir.setPath(qApp->applicationDirPath());
            camitkDir.cdUp();
        }
    }

    return camitkDir.canonicalPath().toUtf8();
}

// ------------- getUserInstallDir (static) -----------------
const QString Core::getUserInstallDir() {
    // obtain (platform specific) application's data/settings directory from settings file
    return QFileInfo(Application::getSettings().fileName()).absolutePath();
}

// ------------- getCurrentWorkingDir (static) -----------------
const QString Core::getCurrentWorkingDir() {
    return QDir::currentPath();
}


// ------------- getPythonStatus (static) -----------------
const QString Core::getPythonStatus() {
#ifndef PYTHON_BINDING
    return "Build without python binding support.";
#else
    return PythonManager::getPythonStatus();
#endif
}

// ------------- getBugReport (static) -----------------
const QString Core::getBugReport() {
    QStringList BugReport;
    QString     breakLine("\n\n");

    BugReport <<
              "About you:                           \n[Present yourself and your project you're working on with CamiTK]" + breakLine;
    BugReport <<
              "Overview:                            \n[Rewrite here a larger and more detailed restatement of your summary]" + breakLine;
    BugReport <<
              "Steps to Reproduce:                  \n[Write here the step - by - step process to reproduce the bug, including file to test (you can attach file on gitlab issue report system)]" + breakLine;
    BugReport <<
              "Actual VS Expected Result:           \n[Write here the result of the step - by - step process and explain why it is not what you expected]" + breakLine;
    BugReport << "Relevant logs and/or screenshots: \n[Paste any relevant logs - please use code blocks (```) to format console output, logs, and code as it's very hard to read otherwise.]" + breakLine;
    BugReport <<
              "Interpretation & Possible fixes:     \n[Write here your interpretation of this bug (If you can, link to the line of code that might be responsible for the problem)]" + breakLine;
    BugReport << "/label ~Bug" + breakLine;
    BugReport <<
              "CamiTK Version:                  \n\n" + Core::getConfig() + breakLine;

    return BugReport.join("\n");
}


// ------------- isDebugBuild (static) -----------------
const bool Core::isDebugBuild() {
#ifdef QT_DEBUG
    return true;
#else
    return false;
#endif
}


}
