// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0

#include "mcumoduleprojectitem.h"

#include <qmljs/qmljssimplereader.h>
#include <utils/algorithm.h>

#include <QJsonArray>
#include <QLoggingCategory>

using namespace Qt::Literals::StringLiterals;

namespace Constants {
namespace QmlDir {
constexpr auto QMLDIR = "qmldir"_L1;
constexpr auto MODULE = "module"_L1;
constexpr auto QML_FILE_FILTER = "*.qml"_L1;
} // namespace QmlDir

namespace Json {
constexpr auto MODULE_URI = "moduleUri"_L1;
constexpr auto QML_FILES = "qmlFiles"_L1;
constexpr auto QMLPROJECT_PATH = "qmlProjectPath"_L1;
} // namespace Json

namespace QmlProject {
constexpr auto QMLPROJECT_EXTENSION = ".qmlproject"_L1;
constexpr auto MCU_MODULE = "MCU.Module"_L1;
constexpr auto URI = "uri"_L1;
constexpr auto QML_FILES = "QmlFiles"_L1;
constexpr auto FILES = "files"_L1;

const auto QMLPROJECT_TEMPLATE = QString(R"(/* File generated by Qt Design Studio */

import QmlProject 1.3
Project {
    MCU.Module {
        uri: %1
    }
    QmlFiles {
        files: [
            %2
        ]
    }
}
)");
} // namespace QmlProject
} // namespace Constants

namespace {
Q_LOGGING_CATEGORY(log, "QmlProjectManager.McuModuleProjectItem")

bool isValidQmlProjectPath(const Utils::FilePath &path)
{
    return path.endsWith(Constants::QmlProject::QMLPROJECT_EXTENSION)
           && (path.ensureExistingFile() || path.parentDir().isWritableDir());
}

QJsonObject parseQmlProjectFile(const Utils::FilePath &qmlproject)
{
    auto qmlprojectPathStr = qmlproject.toFSPathString();

    if (!qmlproject.exists()) {
        qCCritical(log) << "qmlproject file not found:" << qmlprojectPathStr;
        return {};
    }

    QmlJS::SimpleReader reader;
    QmlJS::SimpleReaderNode::Ptr rootNode = reader.readFile(qmlprojectPathStr);
    if (!reader.errors().isEmpty() || !rootNode->isValid()) {
        qCCritical(log) << "Unable to parse:" << qmlprojectPathStr;
        qCCritical(log) << reader.errors();
        return {};
    }

    QJsonObject result;
    result.insert(Constants::Json::QMLPROJECT_PATH, qmlprojectPathStr);

    auto checkNodeName = [](const QString &node, const QString &expecedName) {
        return node.compare(expecedName, Qt::CaseInsensitive) == 0;
    };

    //expected just two nodes: MCU.Module and QmlFiles
    for (const auto &childNode : rootNode->children()) {
        auto nodeName = childNode->name();
        if (checkNodeName(nodeName, Constants::QmlProject::MCU_MODULE)) {
            result.insert(Constants::Json::MODULE_URI,
                          childNode->property(Constants::QmlProject::URI).value.toString());
        } else if (checkNodeName(nodeName, Constants::QmlProject::QML_FILES)) {
            result.insert(Constants::Json::QML_FILES,
                          childNode->property(Constants::QmlProject::FILES).value.toJsonArray());
        } else {
            qCWarning(log) << "Unsupported node:" << nodeName;
        }
    }

    return result;
}
} // namespace

namespace QmlProjectManager {
McuModuleProjectItem::McuModuleProjectItem(const QJsonObject &project)
    : m_project(project)
{}

McuModuleProjectItem::McuModuleProjectItem(const Utils::FilePath &qmlprojectFile)
    : m_qmlProjectFile(qmlprojectFile)
    , m_project(parseQmlProjectFile(m_qmlProjectFile))
{
}

std::optional<McuModuleProjectItem> McuModuleProjectItem::fromQmldirModule(const Utils::FilePath &qmldirFile)
{
    auto qmldirFileStr = qmldirFile.toFSPathString();

    // check qmldirFile
    if (!qmldirFile.exists()) {
        qCWarning(log) << "File not found:" << qmldirFileStr;
        return {};
    }
    if (qmldirFile.fileName() != Constants::QmlDir::QMLDIR) {
        qCWarning(log) << "It's not qmldir file:" << qmldirFileStr;
        return {};
    }

    auto qmldirContents = qmldirFile.fileContents();
    if (!qmldirContents) {
        qCWarning(log) << "Unable to read the file:" << qmldirFileStr
                       << ", error:" << qmldirContents.error();
        return {};
    }

    // find module name
    QByteArray fileContents = qmldirContents.value();
    QTextStream ts(fileContents);
    QString moduleName;

    while (!ts.atEnd()) {
        QString line = ts.readLine().trimmed();
        if (line.startsWith(Constants::QmlDir::MODULE, Qt::CaseInsensitive)) {
            auto list = line.split(' ');
            if (list.size() != 2) {
                qCWarning(log) << "Invalid module identifier:" << line;
                return {};
            }
            moduleName = list.last();
            break;
        }
    }

    if (moduleName.isEmpty()) {
        qCWarning(log) << "Module name not found in the qmldir";
        return {};
    }

    // list qml files
    const auto qmldirParent = qmldirFile.parentDir();
    auto qmlDirEntries = qmldirParent.dirEntries(Utils::FileFilter{{Constants::QmlDir::QML_FILE_FILTER},
                                                                   QDir::NoFilter,
                                                                   QDirIterator::Subdirectories});
    if (qmlDirEntries.empty()) {
        qCWarning(log) << "No qml files found in:" << qmldirParent;
        return {};
    }
    auto qmlFiles = Utils::transform<QStringList>(qmlDirEntries, [qmldirParent](const Utils::FilePath &path) {
        return path.relativePathFrom(qmldirParent).toFSPathString();
    });

    // build mcu module project
    QJsonObject result;
    result.insert(Constants::Json::MODULE_URI, moduleName);
    result.insert(Constants::Json::QML_FILES, QJsonArray::fromStringList(qmlFiles));

    auto filename = moduleName.replace('.', '_');
    auto qmlprojectPath = qmldirParent.resolvePath(
        Utils::FilePath::fromString(filename + Constants::QmlProject::QMLPROJECT_EXTENSION));
    result.insert(Constants::Json::QMLPROJECT_PATH, qmlprojectPath.toFSPathString());

    return McuModuleProjectItem(result);
}

bool McuModuleProjectItem::isValid() const noexcept
{
    return !uri().isEmpty() && !qmlFiles().isEmpty() && isValidQmlProjectPath(qmlProjectPath());
}

QString McuModuleProjectItem::uri() const noexcept
{
    return m_project[Constants::Json::MODULE_URI].toString();
}

void McuModuleProjectItem::setUri(const QString &moduleUri)
{
    m_project[Constants::Json::MODULE_URI] = moduleUri;
}

QStringList McuModuleProjectItem::qmlFiles() const noexcept
{
    return m_project[Constants::Json::QML_FILES].toVariant().toStringList();
}

void McuModuleProjectItem::setQmlFiles(const QStringList &files)
{
    m_project[Constants::Json::QML_FILES] = QJsonArray::fromStringList(files);
}

Utils::FilePath McuModuleProjectItem::qmlProjectPath() const noexcept
{
    return Utils::FilePath::fromString(m_project[Constants::Json::QMLPROJECT_PATH].toString());
}

void McuModuleProjectItem::setQmlProjectPath(const Utils::FilePath &path)
{
    m_project[Constants::Json::QMLPROJECT_PATH] = path.toFSPathString();
}

QJsonObject McuModuleProjectItem::project() const noexcept
{
    return m_project;
}

bool McuModuleProjectItem::saveQmlProjectFile() const
{
    if (!isValid()) {
        return false;
    }

    auto path = qmlProjectPath();
    if (path.exists()) {
        if (McuModuleProjectItem old(path); old == *this) {
            return false;
        }
    }

    QTC_ASSERT_EXPECTED(path.writeFileContents(jsonToQmlproject()), return false);
    return true;
}

bool McuModuleProjectItem::operator==(const McuModuleProjectItem &other) const noexcept
{
    return this->project() == other.project();
}

QByteArray McuModuleProjectItem::jsonToQmlproject() const
{
    auto quoted = [](const QString &s) { return QString("\"%1\"").arg(s); };
    auto indent = [](int tabs = 1) { return QString(" ").repeated(tabs * 4); };
    auto quotedQmlFiles = Utils::transform<QStringList>(qmlFiles(), [quoted](const QString &file) {
        return quoted(file);
    });

    QString qmlFilesSeparator;
    QTextStream ts(&qmlFilesSeparator);
    ts << "," << Qt::endl << indent(3);

    return Constants::QmlProject::QMLPROJECT_TEMPLATE
        .arg(quoted(uri()), quotedQmlFiles.join(qmlFilesSeparator))
        .toUtf8();
}
} // namespace QmlProjectManager
