// 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 <QVector>



#include <App/Application.h>

#include "MaterialFilter.h"
#include "MaterialLibrary.h"
#include "MaterialLoader.h"
#include "MaterialManager.h"
#include "Materials.h"
#include "ModelManager.h"


using namespace Materials;

/* TRANSLATOR Material::Materials */

TYPESYSTEM_SOURCE(Materials::MaterialLibrary, Base::BaseClass)

MaterialLibrary::MaterialLibrary(const QString& libraryName, const QString& icon, bool readOnly)
    : Library(libraryName, icon, readOnly)
{}

MaterialLibrary::MaterialLibrary(const QString& libraryName,
                                 const QString& dir,
                                 const QString& icon,
                                 bool readOnly)
    : Library(libraryName, dir, icon, readOnly)
{}

MaterialLibrary::MaterialLibrary(const Library& library)
    : Library(library)
{}

std::shared_ptr<std::map<QString, std::shared_ptr<MaterialTreeNode>>>
MaterialLibrary::getMaterialTree(const Materials::MaterialFilter& filter,
                                 const Materials::MaterialFilterOptions& options) const
{
    std::shared_ptr<std::map<QString, std::shared_ptr<MaterialTreeNode>>> materialTree =
        std::make_shared<std::map<QString, std::shared_ptr<MaterialTreeNode>>>();

    auto materials = MaterialManager::getManager().libraryMaterials(getName(), filter, options, isLocal());
    for (auto& it : *materials) {
        auto uuid = it.getUUID();
        auto path = it.getPath();
        auto filename = it.getName();

        QStringList list = path.split(QStringLiteral("/"));

        // Start at the root
        std::shared_ptr<std::map<QString, std::shared_ptr<MaterialTreeNode>>> node =
            materialTree;
        for (auto& itp : list) {
            if (!itp.isEmpty()) {
                // Add the folder only if it's not already there
                if (!node->contains(itp)) {
                    auto mapPtr = std::make_shared<
                        std::map<QString, std::shared_ptr<MaterialTreeNode>>>();
                    std::shared_ptr<MaterialTreeNode> child =
                        std::make_shared<MaterialTreeNode>();
                    child->setFolder(mapPtr);
                    child->setReadOnly(isReadOnly());
                    (*node)[itp] = child;
                    node = mapPtr;
                }
                else {
                    node = (*node)[itp]->getFolder();
                }
            }
        }
        std::shared_ptr<MaterialTreeNode> child = std::make_shared<MaterialTreeNode>();
        child->setUUID(uuid);
        child->setReadOnly(isReadOnly());
        if (isLocal()) {
            auto material = MaterialManager::getManager().getMaterial(uuid);
            child->setOldFormat(material->isOldFormat());
        }
        (*node)[filename] = child;
    }

    // // Empty folders aren't included in _materialPathMap, so we add them by looking at the file
    // // system
    // if (!filter || options.includeEmptyFolders()) {
    //     if (isLocal()) {
    //         auto& materialLibrary =
    //             *(reinterpret_cast<const Materials::MaterialLibraryLocal*>(this));
    //         auto folderList = MaterialLoader::getMaterialFolders(materialLibrary);
    //         for (auto& folder : *folderList) {
    //             QStringList list = folder.split(QStringLiteral("/"));

    //             // Start at the root
    //             auto node = materialTree;
    //             for (auto& itp : list) {
    //                 // Add the folder only if it's not already there
    //                 if (!node->contains(itp)) {
    //                     std::shared_ptr<std::map<QString, std::shared_ptr<MaterialTreeNode>>>
    //                         mapPtr = std::make_shared<
    //                             std::map<QString, std::shared_ptr<MaterialTreeNode>>>();
    //                     std::shared_ptr<MaterialTreeNode> child =
    //                         std::make_shared<MaterialTreeNode>();
    //                     child->setFolder(mapPtr);
    //                     (*node)[itp] = child;
    //                     node = mapPtr;
    //                 }
    //                 else {
    //                     node = (*node)[itp]->getFolder();
    //                 }
    //             }
    //         }
    //     }
    // }

    return materialTree;
}

/* TRANSLATOR Material::Materials */

TYPESYSTEM_SOURCE(Materials::MaterialLibraryLocal, Materials::MaterialLibrary)

MaterialLibraryLocal::MaterialLibraryLocal(const QString& libraryName,
                                           const QString& dir,
                                           const QString& icon,
                                           bool readOnly)
    : MaterialLibrary(libraryName, dir, icon, readOnly)
    , _materialPathMap(std::make_unique<std::map<QString, std::shared_ptr<Material>>>())
{
    setLocal(true);
}

void MaterialLibraryLocal::createFolder(const QString& path)
{
    QString filePath = getLocalPath(path);

    QDir fileDir(filePath);
    if (!fileDir.exists()) {
        if (!fileDir.mkpath(filePath)) {
            Base::Console().error("Unable to create directory path '%s'\n",
                                  filePath.toStdString().c_str());
        }
    }
}

void MaterialLibraryLocal::renameFolder(const QString& oldPath, const QString& newPath)
{
    QString filePath = getLocalPath(oldPath);
    QString newFilePath = getLocalPath(newPath);

    QDir fileDir(filePath);
    if (fileDir.exists()) {
        if (!fileDir.rename(filePath, newFilePath)) {
            Base::Console().error("Unable to rename directory path '%s'\n",
                                  filePath.toStdString().c_str());
        }
    }

    updatePaths(oldPath, newPath);
}

void MaterialLibraryLocal::deleteRecursive(const QString& path)
{
    if (isRoot(path)) {
        return;
    }

    QString filePath = getLocalPath(path);
    auto& manager = MaterialManager::getManager();

    QFileInfo info(filePath);
    if (info.isDir()) {
        deleteDir(manager, filePath);
    }
    else {
        deleteFile(manager, filePath);
    }
}

// This accepts the filesystem path as returned from getLocalPath
void MaterialLibraryLocal::deleteDir(MaterialManager& manager, const QString& path)
{
    // Remove the children first
    QDirIterator it(path, QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot);

    // Add paths to a list so there are no iterator errors
    QVector<QString> dirList;
    QVector<QString> fileList;
    while (it.hasNext()) {
        auto pathname = it.next();
        QFileInfo file(pathname);
        if (file.isFile()) {
            fileList.push_back(pathname);
        }
        else if (file.isDir()) {
            dirList.push_back(pathname);
        }
    }

    // Remove the subdirs first
    while (!dirList.isEmpty()) {
        QString dirPath = dirList.takeFirst();
        deleteDir(manager, dirPath);
    }

    // Remove the files
    while (!fileList.isEmpty()) {
        QString filePath = fileList.takeFirst();
        deleteFile(manager, filePath);
    }

    // Finally, remove ourself
    QDir dir;
    if (!dir.rmdir(path)) {
        throw DeleteError(path);
    }
}

// This accepts the filesystem path as returned from getLocalPath
void MaterialLibraryLocal::deleteFile(MaterialManager& manager, const QString& path)
{
    if (QFile::remove(path)) {
        // Remove from the map
        QString rPath = getRelativePath(path);
        try {
            auto material = getMaterialByPath(rPath);
            manager.remove(material->getUUID());
        }
        catch (const MaterialNotFound&) {
            Base::Console().log("Unable to remove file from materials list\n");
        }
        _materialPathMap->erase(rPath);
    }
    else {
        QString error = QStringLiteral("DeleteError: Unable to delete ") + path;
        throw DeleteError(error);
    }
}

void MaterialLibraryLocal::updatePaths(const QString& oldPath, const QString& newPath)
{
    // Update the path map
    QString op = getRelativePath(oldPath);
    QString np = getRelativePath(newPath);
    std::unique_ptr<std::map<QString, std::shared_ptr<Material>>> pathMap =
        std::make_unique<std::map<QString, std::shared_ptr<Material>>>();
    for (auto& itp : *_materialPathMap) {
        QString path = itp.first;
        if (path.startsWith(op)) {
            path = np + path.remove(0, op.size());
        }
        itp.second->setDirectory(path);
        (*pathMap)[path] = itp.second;
    }

    _materialPathMap = std::move(pathMap);
}

std::shared_ptr<Material>
MaterialLibraryLocal::saveMaterial(const std::shared_ptr<Material>& material,
                                   const QString& path,
                                   bool overwrite,
                                   bool saveAsCopy,
                                   bool saveInherited)
{
    QString filePath = getLocalPath(path);
    QFile file(filePath);

    QFileInfo info(file);
    QDir fileDir(info.path());
    if (!fileDir.exists()) {
        if (!fileDir.mkpath(info.path())) {
            Base::Console().error("Unable to create directory path '%s'\n",
                                  info.path().toStdString().c_str());
        }
    }

    if (info.exists()) {
        if (!overwrite) {
            Base::Console().error("File already exists '%s'\n", info.path().toStdString().c_str());
            throw MaterialExists();
        }
    }

    if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream stream(&file);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
        stream.setCodec("UTF-8");
#endif
        stream.setGenerateByteOrderMark(true);

        // Write the contents
        material->setName(info.fileName().remove(QStringLiteral(".FCMat"), Qt::CaseInsensitive));
        material->setLibrary(getptr());
        material->setDirectory(getRelativePath(path));
        material->save(stream, overwrite, saveAsCopy, saveInherited);
    }

    return addMaterial(material, path);
}

bool MaterialLibraryLocal::fileExists(const QString& path) const
{
    QString filePath = getLocalPath(path);
    QFileInfo info(filePath);

    return info.exists();
}

std::shared_ptr<Material>
MaterialLibraryLocal::addMaterial(const std::shared_ptr<Material>& material, const QString& path)
{
    QString filePath = getRelativePath(path);
    QFileInfo info(filePath);
    std::shared_ptr<Material> newMaterial = std::make_shared<Material>(*material);
    newMaterial->setLibrary(getptr());
    newMaterial->setDirectory(getLibraryPath(filePath, info.fileName()));
    newMaterial->setFilename(info.fileName());

    (*_materialPathMap)[filePath] = newMaterial;

    return newMaterial;
}

std::shared_ptr<Material> MaterialLibraryLocal::getMaterialByPath(const QString& path) const
{
    QString filePath = getRelativePath(path);

    auto search = _materialPathMap->find(filePath);
    if (search != _materialPathMap->end()) {
        return search->second;
    }

    throw MaterialNotFound();
}

QString MaterialLibraryLocal::getUUIDFromPath(const QString& path) const
{
    QString filePath = getRelativePath(path);

    auto search = _materialPathMap->find(filePath);
    if (search != _materialPathMap->end()) {
        return search->second->getUUID();
    }

    throw MaterialNotFound();
}
