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

#include "projectprocessor.h"
#include "qrcreader.h"
#include <translator.h>
#include <trlib/trparser.h>

#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qregularexpression.h>
#include <QtCore/qset.h>

#include <iostream>

using namespace Qt::StringLiterals;

namespace {

static void printOut(const QString &out)
{
    std::cout << qPrintable(out);
}

static void printErr(const QString &out)
{
    std::cerr << qPrintable(out);
}

static void printWarning(UpdateOptions options,
                const QString &msg,
                const QString &warningMsg = {},
                const QString &errorMsg = {})
{
    QString text = msg;
    if (options & Werror) {
        text.prepend("lupdate error: "_L1);
        if (!errorMsg.isEmpty())
            text.append(" "_L1).append(errorMsg);
    } else {
        text.prepend("lupdate warning: "_L1);
        if (!warningMsg.isEmpty())
            text.append(" "_L1).append(warningMsg);
    }

    printErr(text);
}

static bool readFileContent(const QString &filePath, QByteArray *content, QString *errorString)
{
    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        *errorString = file.errorString();
        return false;
    }
    *content = file.readAll();
    return true;
}

static bool readFileContent(const QString &filePath, QString *content, QString *errorString)
{
    QByteArray ba;
    if (!readFileContent(filePath, &ba, errorString))
        return false;
    *content = QString::fromUtf8(ba);
    return true;
}

static void removeExcludedSources(Projects &projects)
{
    for (Project &project : projects) {
        for (const QRegularExpression &rx : std::as_const(project.excluded)) {
            for (auto it = project.sources.begin(); it != project.sources.end(); ) {
                if (rx.match(*it).hasMatch())
                    it = project.sources.erase(it);
                else
                    ++it;
            }
        }
        removeExcludedSources(project.subProjects);
    }
}


// Remove .qrc files from the project and return them as absolute paths.
static QStringList extractQrcFiles(Project &project)
{
    auto it = project.sources.begin();
    QStringList qrcFiles;
    while (it != project.sources.end()) {
        QFileInfo fi(*it);
        QString fn = QDir::cleanPath(fi.absoluteFilePath());
        if (fn.endsWith(".qrc"_L1, Qt::CaseInsensitive)) {
            qrcFiles += fn;
            it = project.sources.erase(it);
        } else {
            ++it;
        }
    }
    return qrcFiles;
}

// Replace all .qrc files in the project with their content.
static void expandQrcFiles(Project &project)
{
    for (const QString &qrcFile : extractQrcFiles(project))
        project.sources << getSourceFilesFromQrc(qrcFile);
}

static QSet<QString> projectRoots(const QString &projectFile, const QStringList &sourceFiles)
{
    const QString proPath = QFileInfo(projectFile).path();
    QSet<QString> sourceDirs;
    sourceDirs.insert(proPath + u'/');
    for (const QString &sf : sourceFiles)
        sourceDirs.insert(sf.left(sf.lastIndexOf(u'/') + 1));
    QStringList rootList = sourceDirs.values();
    rootList.sort();
    for (int prev = 0, curr = 1; curr < rootList.size(); )
        if (rootList.at(curr).startsWith(rootList.at(prev)))
            rootList.removeAt(curr);
        else
            prev = curr++;
    return QSet<QString>(rootList.cbegin(), rootList.cend());
}

static bool updateTsFiles(const Translator &fetchedTor, const QStringList &tsFileNames,
    const QStringList &alienFiles,
    const QString &sourceLanguage, const QString &targetLanguage,
    UpdateOptions options)
{
    bool fail = false;
    for (int i = 0; i < fetchedTor.messageCount(); i++) {
        const TranslatorMessage &msg = fetchedTor.constMessage(i);
        if (!msg.id().isEmpty() && msg.sourceText().isEmpty()) {
            printWarning(options,
                         "Message with id '%1' has no source.\n"_L1.arg(msg.id()));
            if (options & Werror)
                return !fail;
        }
    }
    QList<Translator> aliens;
    for (const QString &fileName : alienFiles) {
        ConversionData cd;
        Translator tor;
        if (!tor.load(fileName, cd, "auto"_L1)) {
            printErr(cd.error());
            fail = true;
            continue;
        }
        tor.resolveDuplicates();
        aliens << tor;
    }
    QDir dir;
    QString err;
    for (const QString &fileName : tsFileNames) {
        QString fn = dir.relativeFilePath(fileName);
        ConversionData cd;
        Translator tor;
        cd.m_sortContexts = !(options & NoSort);
        cd.m_sortMessages = options & SortMessages;
        if (QFile(fileName).exists()) {
            if (!tor.load(fileName, cd, "auto"_L1)) {
                printErr(cd.error());
                fail = true;
                continue;
            }
            tor.resolveDuplicates();
            cd.clearErrors();
            if (!targetLanguage.isEmpty() && targetLanguage != tor.languageCode()) {
                printWarning(options,
                             "Specified target language '%1' disagrees with"
                                            " existing file's language '%2'.\n"_L1
                                     .arg(targetLanguage, tor.languageCode()),
                             u"Ignoring.\n"_s);
                if (options & Werror)
                    return !fail;
            }
            if (!sourceLanguage.isEmpty() && sourceLanguage != tor.sourceLanguageCode()) {
                printWarning(options,
                             "Specified source language '%1' disagrees with"
                                     " existing file's language '%2'.\n"_L1
                                     .arg(sourceLanguage, tor.sourceLanguageCode()),
                             u"Ignoring.\n"_s);
                if (options & Werror)
                    return !fail;
            }
            // If there is translation in the file, the language should be recognized
            // (when the language is not recognized, plural translations are lost)
            if (tor.translationsExist()) {

                if (tor.languageCode().isEmpty()) {
                    printErr("File %1 won't be updated: it does not specify any "
                             "target languages. To set a target language, open "
                             "the file in Qt Linguist.\n"_L1.arg(fileName));
                    continue;
                }
                QLocale::Language l;
                QLocale::Territory c;
                tor.languageAndTerritory(tor.languageCode(), &l, &c);
                QStringList forms;
                if (!getNumerusInfo(l, c, 0, &forms, 0)) {
                    printErr(QStringLiteral("File %1 won't be updated: it contains translation but the"
                    " target language is not recognized\n").arg(fileName));
                    continue;
                }
            }
        } else {
            if (!targetLanguage.isEmpty())
                tor.setLanguageCode(targetLanguage);
            else
                tor.setLanguageCode(Translator::guessLanguageCodeFromFileName(fileName));
            if (!sourceLanguage.isEmpty())
                tor.setSourceLanguageCode(sourceLanguage);
        }
        tor.makeFileNamesAbsolute(QFileInfo(fileName).absoluteDir());
        if (options & NoLocations)
            tor.setLocationsType(Translator::NoLocations);
        else if (options & RelativeLocations)
            tor.setLocationsType(Translator::RelativeLocations);
        else if (options & AbsoluteLocations)
            tor.setLocationsType(Translator::AbsoluteLocations);
        if (options & Verbose)
            printOut(QStringLiteral("Updating '%1'...\n").arg(fn));

        UpdateOptions theseOptions = options;
        if (tor.locationsType() == Translator::NoLocations) // Could be set from file
            theseOptions |= NoLocations;
        Translator out = merge(tor, fetchedTor, aliens, theseOptions, err);

        if ((options & Verbose) && !err.isEmpty()) {
            printOut(err);
            err.clear();
        }
        if (options & PluralOnly) {
            if (options & Verbose)
                printOut(QStringLiteral("Stripping non plural forms in '%1'...\n").arg(fn));
            out.stripNonPluralForms();
        }
        if (options & NoObsolete)
            out.stripObsoleteMessages();
        out.stripEmptyContexts();

        out.normalizeTranslations(cd);
        if (!cd.errors().isEmpty()) {
            printErr(cd.error());
            cd.clearErrors();
        }
        if (!out.save(fileName, cd, "auto"_L1)) {
            printErr(cd.error());
            fail = true;
        }
    }
    return !fail;
}

class ProjectProcessor
{
public:
    ProjectProcessor(const QString &sourceLanguage,
                     const QString &targetLanguage)
        : m_sourceLanguage(sourceLanguage),
          m_targetLanguage(targetLanguage)
    {
    }

    bool processProjects(bool topLevel, UpdateOptions options, const Projects &projects,
                         bool nestComplain, Translator *parentTor) const
    {
        bool ok = true;
        for (const Project &prj : projects)
            ok &= processProject(options, prj, topLevel, nestComplain, parentTor);
        return ok;
    }

private:

    bool processProject(UpdateOptions options, const Project &prj, bool topLevel,
                        bool nestComplain, Translator *parentTor) const
    {
        bool ok = true;
        QString codecForSource = prj.codec.toLower();
        if (!codecForSource.isEmpty()) {
            if (codecForSource == "utf-16"_L1 || codecForSource == "utf16"_L1) {
                options |= SourceIsUtf16;
            } else if (codecForSource == "utf-8"_L1 || codecForSource == "utf8"_L1) {
                options &= ~SourceIsUtf16;
            } else {
                printWarning(
                        options,
                        "Codec for source '%1' is invalid.\n"_L1.arg(codecForSource),
                        u"Falling back to UTF-8.\n"_s);
                if (options & Werror)
                    return ok;
                options &= ~SourceIsUtf16;
            }
        }

        const QString projectFile = prj.filePath;
        const QStringList sources = prj.sources;
        ConversionData cd;
        cd.m_noUiLines = options & NoUiLines;
        cd.m_projectRoots = projectRoots(projectFile, sources);
        QStringList projectRootDirs;
        for (const auto& dir : std::as_const(cd.m_projectRoots))
            projectRootDirs.append(dir);
        cd.m_includePath = prj.includePaths;
        cd.m_excludes = prj.excluded;
        cd.m_sourceIsUtf16 = options & SourceIsUtf16;

        QStringList tsFiles;
        if (prj.translations) {
            tsFiles = *prj.translations;
            if (parentTor) {
                if (topLevel) {
                    printWarning(options, u"Existing top level."_s,
                                 "TS files from command line will "
                                 "override TRANSLATIONS in %1.\n"_L1.arg(projectFile),
                                 u"Terminating the operation.\n"_s);
                    if (options & Werror)
                        return ok;
                    goto noTrans;
                } else if (nestComplain) {
                    printWarning(options,
                                "TS files from command line "
                                "prevent recursing into %1.\n"_L1.arg(projectFile));
                    return ok;
                }
            }
            if (tsFiles.isEmpty()) {
                // This might mean either a buggy PRO file or an intentional detach -
                // we can't know without seeing the actual RHS of the assignment ...
                // Just assume correctness and be silent.
                return ok;
            }
            Translator tor;
            ok = processProjects(false, options, prj.subProjects, false, &tor);
            processSources(tor, sources, cd);
            ok &= updateTsFiles(tor, tsFiles, QStringList(), m_sourceLanguage, m_targetLanguage,
                          options);
            return ok;
        }


      noTrans:
        if (!parentTor) {
            if (topLevel) {
                printWarning(options, u"no TS files specified."_s,
                             "Only diagnostics will be produced for %1.\n"_L1.arg(projectFile),
                             u"Terminating the operation.\n"_s);
                if (options & Werror)
                    return ok;
            }
            Translator tor;
            ok = processProjects(false, options, prj.subProjects, nestComplain, &tor);
            processSources(tor, sources, cd);
        } else {
            ok = processProjects(false, options, prj.subProjects, nestComplain, parentTor);
            processSources(*parentTor, sources, cd);
        }
        return ok;
    }

    QString m_sourceLanguage;
    QString m_targetLanguage;
};

}

QT_BEGIN_NAMESPACE

QStringList getSourceFilesFromQrc(const QString &resourceFile)
{
    if (!QFile::exists(resourceFile))
        return QStringList();
    QString content;
    QString errStr;
    if (!readFileContent(resourceFile, &content, &errStr)) {
        printErr(QStringLiteral("lupdate error: Can not read %1: %2\n").arg(resourceFile, errStr));
        return QStringList();
    }
    ReadQrcResult rqr = readQrcFile(resourceFile, content);
    if (rqr.hasError()) {
        printErr(QStringLiteral("lupdate error: %1:%2: %3\n")
                         .arg(resourceFile, QString::number(rqr.line), rqr.errorString));
    }
    return rqr.files;
}

bool processProjectDescription(
    Projects &projectDescription,
    const QStringList &tsFileNames,
    const QStringList &alienFiles,
    const QString &sourceLanguage,
    const QString &targetLanguage,
    UpdateOptions options)
{
    bool ok = true;

    removeExcludedSources(projectDescription);
    for (Project &project : projectDescription)
        expandQrcFiles(project);

    ProjectProcessor projectProcessor(sourceLanguage, targetLanguage);
    if (!tsFileNames.isEmpty()) {
        Translator fetchedTor;
        ok &= projectProcessor.processProjects(true, options, projectDescription, true, &fetchedTor);
        if (ok) {
            ok &= updateTsFiles(fetchedTor, tsFileNames, alienFiles,
                          sourceLanguage, targetLanguage, options);
        }
    } else {
        ok &= projectProcessor.processProjects(true, options, projectDescription, false, nullptr);
    }
    return ok;
}

bool processSourceFiles(
    const QStringList &sourceFiles,
    const QStringList &tsFileNames,
    const QStringList &alienFiles,
    const QSet<QString> &projectRoots,
    const QStringList &includePath,
    const QMultiHash<QString, QString> &allCSources,
    const QString &sourceLanguage,
    const QString &targetLanguage,
    UpdateOptions options)
{
    Translator fetchedTor;
    ConversionData cd;
    cd.m_noUiLines = options & NoUiLines;
    cd.m_sourceIsUtf16 = options & SourceIsUtf16;
    cd.m_projectRoots = projectRoots;
    cd.m_includePath = includePath;
    cd.m_allCSources = allCSources;
    processSources(fetchedTor, sourceFiles, cd);
    return updateTsFiles(fetchedTor, tsFileNames, alienFiles,
                  sourceLanguage, targetLanguage, options);
}

QT_END_NAMESPACE
