// SPDX-License-Identifier: LGPL-2.1-or-later

/***************************************************************************
 *   Copyright (c) 2023 David Carter <dcarter@david.carter.ca>             *
 *                                                                         *
 *   This file is part of FreeCAD.                                         *
 *                                                                         *
 *   FreeCAD is free software: you can redistribute it and/or modify it    *
 *   under the terms of the GNU Lesser General Public License as           *
 *   published by the Free Software Foundation, either version 2.1 of the  *
 *   License, or (at your option) any later version.                       *
 *                                                                         *
 *   FreeCAD 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 for more details.                       *
 *                                                                         *
 *   You should have received a copy of the GNU Lesser General Public      *
 *   License along with FreeCAD. If not, see                               *
 *   <https://www.gnu.org/licenses/>.                                      *
 *                                                                         *
 **************************************************************************/

#include <QDirIterator>
#include <QFileInfo>
#include <QString>

#include <App/Application.h>
#include <Base/FileInfo.h>
#include <Base/Interpreter.h>
#include <Base/Stream.h>


#include "Model.h"
#include "ModelLoader.h"
#include "ModelManager.h"


using namespace Materials;

ModelEntry::ModelEntry(const std::shared_ptr<ModelLibraryLocal>& library,
                       const QString& baseName,
                       const QString& modelName,
                       const QString& dir,
                       const QString& modelUuid,
                       const YAML::Node& modelData)
    : _library(library)
    , _base(baseName)
    , _name(modelName)
    , _directory(dir)
    , _uuid(modelUuid)
    , _model(modelData)
    , _dereferenced(false)
{}

std::unique_ptr<std::map<QString, std::shared_ptr<ModelEntry>>> ModelLoader::_modelEntryMap =
    nullptr;

ModelLoader::ModelLoader(std::shared_ptr<std::map<QString, std::shared_ptr<Model>>> modelMap,
                         std::shared_ptr<std::list<std::shared_ptr<ModelLibraryLocal>>> libraryList)
    : _modelMap(modelMap)
    , _libraryList(libraryList)
{
    loadLibraries();
}

void ModelLoader::addLibrary(std::shared_ptr<ModelLibraryLocal> model)
{
    _libraryList->push_back(model);
}

const QString ModelLoader::getUUIDFromPath(const QString& path)
{
    QFile file(path);
    if (!file.exists()) {
        throw ModelNotFound();
    }

    try {
        Base::FileInfo fi(path.toStdString());
        Base::ifstream str(fi);
        YAML::Node yamlroot = YAML::Load(str);
        std::string base = "Model";
        if (yamlroot["AppearanceModel"]) {
            base = "AppearanceModel";
        }

        const QString uuid = QString::fromStdString(yamlroot[base]["UUID"].as<std::string>());
        return uuid;
    }
    catch (YAML::Exception&) {
        throw ModelNotFound();
    }
}

std::shared_ptr<ModelEntry> ModelLoader::getModelFromPath(std::shared_ptr<ModelLibrary> library,
                                                          const QString& path) const
{
    QFile file(path);
    if (!file.exists()) {
        throw ModelNotFound();
    }

    YAML::Node yamlroot;
    std::string base = "Model";
    std::string uuid;
    std::string name;
    try {
        Base::FileInfo fi(path.toStdString());
        Base::ifstream str(fi);
        yamlroot = YAML::Load(str);
        if (yamlroot["AppearanceModel"]) {
            base = "AppearanceModel";
        }

        uuid = yamlroot[base]["UUID"].as<std::string>();
        name = yamlroot[base]["Name"].as<std::string>();
    }
    catch (YAML::Exception const&) {
        throw InvalidModel();
    }

    auto localLibrary = std::static_pointer_cast<ModelLibraryLocal>(library);
    std::shared_ptr<ModelEntry> model = std::make_shared<ModelEntry>(localLibrary,
                                                                     QString::fromStdString(base),
                                                                     QString::fromStdString(name),
                                                                     path,
                                                                     QString::fromStdString(uuid),
                                                                     yamlroot);

    return model;
}

void ModelLoader::showYaml(const YAML::Node& yaml) const
{
    std::stringstream out;

    out << yaml;
    std::string logData = out.str();
    Base::Console().log("%s\n", logData.c_str());
}

void ModelLoader::dereference(const QString& uuid,
                              std::shared_ptr<ModelEntry> parent,
                              std::shared_ptr<ModelEntry> child,
                              std::map<std::pair<QString, QString>, QString>* inheritances)
{
    auto parentPtr = parent->getModelPtr();
    auto parentBase = parent->getBase().toStdString();
    auto childYaml = child->getModel();
    auto childBase = child->getBase().toStdString();

    std::set<QString> exclude;
    exclude.insert(QStringLiteral("Name"));
    exclude.insert(QStringLiteral("UUID"));
    exclude.insert(QStringLiteral("URL"));
    exclude.insert(QStringLiteral("Description"));
    exclude.insert(QStringLiteral("DOI"));
    exclude.insert(QStringLiteral("Inherits"));

    auto parentProperties = (*parentPtr)[parentBase];
    auto childProperties = childYaml[childBase];
    for (auto it = childProperties.begin(); it != childProperties.end(); it++) {
        std::string name = it->first.as<std::string>();
        if (!exclude.contains(QString::fromStdString(name))) {
            // showYaml(it->second);
            if (!parentProperties[name]) {
                parentProperties[name] = it->second;
                // parentProperties[name]["Inherits"] = childYaml[childBase]["UUID"];
                (*inheritances)[std::pair<QString, QString>(uuid, QString::fromStdString(name))] =
                    yamlValue(childYaml[childBase], "UUID", "");
            }
        }
    }
    // showYaml(*parentPtr);
}


void ModelLoader::dereference(std::shared_ptr<ModelEntry> model,
                              std::map<std::pair<QString, QString>, QString>* inheritances)
{
    // Avoid recursion
    if (model->getDereferenced()) {
        return;
    }

    auto yamlModel = model->getModel();
    auto base = model->getBase().toStdString();
    if (yamlModel[base]["Inherits"]) {
        auto inherits = yamlModel[base]["Inherits"];
        for (auto it = inherits.begin(); it != inherits.end(); it++) {
            QString nodeName = QString::fromStdString((*it)["UUID"].as<std::string>());

            // This requires that all models have already been loaded undereferenced
            try {
                std::shared_ptr<ModelEntry> child = (*_modelEntryMap)[nodeName];
                dereference(model->getUUID(), model, child, inheritances);
            }
            catch (const std::out_of_range&) {
                Base::Console().log("Unable to find '%s' in model map\n",
                                    nodeName.toStdString().c_str());
            }
        }
    }

    model->markDereferenced();
}

QString ModelLoader::yamlValue(const YAML::Node& node,
                               const std::string& key,
                               const std::string& defaultValue)
{
    if (node[key]) {
        return QString::fromStdString(node[key].as<std::string>());
    }
    return QString::fromStdString(defaultValue);
}

void ModelLoader::addToTree(std::shared_ptr<ModelEntry> model,
                            std::map<std::pair<QString, QString>, QString>* inheritances)
{
    std::set<QString> exclude;
    exclude.insert(QStringLiteral("Name"));
    exclude.insert(QStringLiteral("UUID"));
    exclude.insert(QStringLiteral("URL"));
    exclude.insert(QStringLiteral("Description"));
    exclude.insert(QStringLiteral("DOI"));
    exclude.insert(QStringLiteral("Inherits"));

    auto yamlModel = model->getModel();
    if (!model->getLibrary()->isLocal()) {
        throw InvalidLibrary();
    }
    auto library = model->getLibrary();
    auto base = model->getBase().toStdString();
    auto name = model->getName();
    auto directory = model->getDirectory();
    auto uuid = model->getUUID();

    QString description = yamlValue(yamlModel[base], "Description", "");
    QString url = yamlValue(yamlModel[base], "URL", "");
    QString doi = yamlValue(yamlModel[base], "DOI", "");

    Model::ModelType type =
        (base == "Model") ? Model::ModelType_Physical : Model::ModelType_Appearance;

    Model finalModel(library, type, name, directory, uuid, description, url, doi);

    // Add inheritance list
    if (yamlModel[base]["Inherits"]) {
        auto inherits = yamlModel[base]["Inherits"];
        for (auto it = inherits.begin(); it != inherits.end(); it++) {
            QString nodeName = QString::fromStdString((*it)["UUID"].as<std::string>());

            finalModel.addInheritance(nodeName);
        }
    }

    // Add property list
    auto yamlProperties = yamlModel[base];
    for (auto it = yamlProperties.begin(); it != yamlProperties.end(); it++) {
        std::string propName = it->first.as<std::string>();
        if (!exclude.contains(QString::fromStdString(propName))) {
            // showYaml(it->second);
            auto yamlProp = yamlProperties[propName];
            auto propDisplayName = yamlValue(yamlProp, "DisplayName", "");
            auto propType = yamlValue(yamlProp, "Type", "");
            auto propUnits = yamlValue(yamlProp, "Units", "");
            auto propURL = yamlValue(yamlProp, "URL", "");
            auto propDescription = yamlValue(yamlProp, "Description", "");
            // auto inherits = yamlValue(yamlProp, "Inherits", "");

            ModelProperty property(QString::fromStdString(propName),
                                   propDisplayName,
                                   propType,
                                   propUnits,
                                   propURL,
                                   propDescription);

            if (propType == QStringLiteral("2DArray") || propType == QStringLiteral("3DArray")) {
                // Base::Console().Log("Reading columns\n");
                // Read the columns
                auto cols = yamlProp["Columns"];
                for (const auto& col : cols) {
                    std::string colName = col.first.as<std::string>();
                    // Base::Console().Log("\tColumns '%s'\n", colName.c_str());

                    auto colProp = cols[colName];
                    auto colPropDisplayName = yamlValue(colProp, "DisplayName", "");
                    auto colPropType = yamlValue(colProp, "Type", "");
                    auto colPropUnits = yamlValue(colProp, "Units", "");
                    auto colPropURL = yamlValue(colProp, "URL", "");
                    auto colPropDescription = yamlValue(colProp, "Description", "");
                    ModelProperty colProperty(QString::fromStdString(colName),
                                              colPropDisplayName,
                                              colPropType,
                                              colPropUnits,
                                              colPropURL,
                                              colPropDescription);

                    property.addColumn(colProperty);
                }
            }

            auto key = std::pair<QString, QString>(uuid, QString::fromStdString(propName));
            if (inheritances->contains(key)) {
                property.setInheritance((*inheritances)[key]);
            }

            finalModel.addProperty(property);
        }
    }

    (*_modelMap)[uuid] = library->addModel(finalModel, directory);
}

void ModelLoader::loadLibrary(std::shared_ptr<ModelLibraryLocal> library)
{
    if (_modelEntryMap == nullptr) {
        _modelEntryMap = std::make_unique<std::map<QString, std::shared_ptr<ModelEntry>>>();
    }

    QDirIterator it(library->getDirectory(), QDirIterator::Subdirectories);
    while (it.hasNext()) {
        auto pathname = it.next();
        QFileInfo file(pathname);
        if (file.isFile()) {
            if (file.suffix().toStdString() == "yml") {
                try {
                    auto model = getModelFromPath(library, file.canonicalFilePath());
                    (*_modelEntryMap)[model->getUUID()] = model;
                    // showYaml(model->getModel());
                }
                catch (InvalidModel const&) {
                    Base::Console().log("Invalid model '%s'\n", pathname.toStdString().c_str());
                }
            }
        }
    }

    std::map<std::pair<QString, QString>, QString> inheritances;
    for (auto it = _modelEntryMap->begin(); it != _modelEntryMap->end(); it++) {
        dereference(it->second, &inheritances);
    }

    for (auto it = _modelEntryMap->begin(); it != _modelEntryMap->end(); it++) {
        addToTree(it->second, &inheritances);
    }
}

void ModelLoader::loadLibraries()
{
    getModelLibraries();
    if (_libraryList) {
        for (auto it = _libraryList->begin(); it != _libraryList->end(); it++) {
            loadLibrary(*it);
        }
    }
}

void ModelLoader::getModelLibraries()
{
    auto param = App::GetApplication().GetParameterGroupByPath(
        "User parameter:BaseApp/Preferences/Mod/Material/Resources");
    bool useBuiltInMaterials = param->GetBool("UseBuiltInMaterials", true);
    bool useMatFromModules = param->GetBool("UseMaterialsFromWorkbenches", true);
    bool useMatFromConfigDir = param->GetBool("UseMaterialsFromConfigDir", true);
    bool useMatFromCustomDir = param->GetBool("UseMaterialsFromCustomDir", true);

    if (useBuiltInMaterials) {
        QString resourceDir = QString::fromStdString(App::Application::getResourceDir()
                                                     + "/Mod/Material/Resources/Models");
        auto libData = std::make_shared<ModelLibraryLocal>(QStringLiteral("System"),
                                                      resourceDir,
                                                      QStringLiteral(":/icons/freecad.svg"));
        _libraryList->push_back(libData);
    }

    if (useMatFromModules) {
        auto moduleParam = App::GetApplication().GetParameterGroupByPath(
            "User parameter:BaseApp/Preferences/Mod/Material/Resources/Modules");
        for (auto& group : moduleParam->GetGroups()) {
            // auto module = moduleParam->GetGroup(group->GetGroupName());
            auto moduleName = QString::fromStdString(group->GetGroupName());
            auto modelDir = QString::fromStdString(group->GetASCII("ModuleModelDir", ""));
            auto modelIcon = QString::fromStdString(group->GetASCII("ModuleIcon", ""));

            if (modelDir.length() > 0) {
                QDir dir(modelDir);
                if (dir.exists()) {
                    auto libData = std::make_shared<ModelLibraryLocal>(moduleName, modelDir, modelIcon);
                    _libraryList->push_back(libData);
                }
            }
        }
    }

    if (useMatFromConfigDir) {
        QString resourceDir =
            QString::fromStdString(App::Application::getUserAppDataDir() + "/Models");
        if (!resourceDir.isEmpty()) {
            QDir materialDir(resourceDir);
            if (materialDir.exists()) {
                auto libData = std::make_shared<ModelLibraryLocal>(
                    QStringLiteral("User"),
                    resourceDir,
                    QStringLiteral(":/icons/preferences-general.svg"));
                _libraryList->push_back(libData);
            }
        }
    }

    if (useMatFromCustomDir) {
        QString resourceDir = QString::fromStdString(param->GetASCII("CustomMaterialsDir", ""));
        if (!resourceDir.isEmpty()) {
            QDir materialDir(resourceDir);
            if (materialDir.exists()) {
                auto libData = std::make_shared<ModelLibraryLocal>(QStringLiteral("Custom"),
                                                              resourceDir,
                                                              QStringLiteral(":/icons/user.svg"));
                _libraryList->push_back(libData);
            }
        }
    }
}
