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

#include "qmakeglobals.h"

#include "qmakeevaluator.h"
#include "ioutils.h"

#include <qbytearray.h>
#include <qdatetime.h>
#include <qdebug.h>
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qlist.h>
#include <qset.h>
#include <qstack.h>
#include <qstring.h>
#include <qstringlist.h>
#include <qtextstream.h>
#ifdef PROEVALUATOR_THREAD_SAFE
# include <qthreadpool.h>
#endif

#ifdef Q_OS_UNIX
#include <unistd.h>
#include <sys/utsname.h>
#else
#include <qt_windows.h>
#endif
#include <stdio.h>
#include <stdlib.h>

#ifdef Q_OS_WIN32
#define QT_POPEN _popen
#define QT_POPEN_READ "rb"
#define QT_PCLOSE _pclose
#else
#define QT_POPEN popen
#define QT_POPEN_READ "r"
#define QT_PCLOSE pclose
#endif

QT_BEGIN_NAMESPACE
using namespace QMakeInternal; // for IoUtils

#define fL1S(s) QString::fromLatin1(s)

QMakeGlobals::QMakeGlobals()
{
    do_cache = true;

#ifdef PROEVALUATOR_DEBUG
    debugLevel = 0;
#endif
#ifdef Q_OS_WIN
    dirlist_sep = QLatin1Char(';');
    dir_sep = QLatin1Char('\\');
#else
    dirlist_sep = QLatin1Char(':');
    dir_sep = QLatin1Char('/');
#endif
}

QMakeGlobals::~QMakeGlobals()
{
    qDeleteAll(baseEnvs);
}

QString QMakeGlobals::cleanSpec(QMakeCmdLineParserState &state, const QString &spec)
{
    QString ret = QDir::cleanPath(spec);
    if (ret.contains(QLatin1Char('/'))) {
        QString absRet = IoUtils::resolvePath(state.pwd, ret);
        if (QFile::exists(absRet))
            ret = absRet;
    }
    return ret;
}

/*
 * Return value meanings:
 * ArgumentUnknown    The argument at *pos was not handled by this function.
 *                    Leave it to the caller to handle this argument.
 * ArgumentMalformed  There was an error detected.
 * ArgumentsOk        All arguments were known. There are no arguments left to handle.
 */
QMakeGlobals::ArgumentReturn QMakeGlobals::addCommandLineArguments(
        QMakeCmdLineParserState &state, QStringList &args, int *pos)
{
    enum { ArgNone, ArgConfig, ArgSpec, ArgXSpec, ArgTmpl, ArgTmplPfx, ArgCache, ArgQtConf } argState = ArgNone;
    for (; *pos < args.size(); (*pos)++) {
        QString arg = args.at(*pos);
        switch (argState) {
        case ArgConfig:
            state.configs[state.phase] << arg;
            break;
        case ArgSpec:
            qmakespec = args[*pos] = cleanSpec(state, arg);
            break;
        case ArgXSpec:
            xqmakespec = args[*pos] = cleanSpec(state, arg);
            break;
        case ArgTmpl:
            user_template = arg;
            break;
        case ArgTmplPfx:
            user_template_prefix = arg;
            break;
        case ArgCache:
            cachefile = args[*pos] = IoUtils::resolvePath(state.pwd, arg);
            break;
        case ArgQtConf:
            qtconf = args[*pos] = IoUtils::resolvePath(state.pwd, arg);
            break;
        default:
            if (arg.startsWith(QLatin1Char('-'))) {
                if (arg == QLatin1String("--")) {
                    state.extraargs = args.mid(*pos + 1);
                    args.erase(args.begin() + *pos, args.end());
                    return ArgumentsOk;
                }
                if (arg == QLatin1String("-early"))
                    state.phase = QMakeEvalEarly;
                else if (arg == QLatin1String("-before"))
                    state.phase = QMakeEvalBefore;
                else if (arg == QLatin1String("-after"))
                    state.phase = QMakeEvalAfter;
                else if (arg == QLatin1String("-late"))
                    state.phase = QMakeEvalLate;
                else if (arg == QLatin1String("-config"))
                    argState = ArgConfig;
                else if (arg == QLatin1String("-nocache"))
                    do_cache = false;
                else if (arg == QLatin1String("-cache"))
                    argState = ArgCache;
                else if (arg == QLatin1String("-qtconf"))
                    argState = ArgQtConf;
                else if (arg == QLatin1String("-platform") || arg == QLatin1String("-spec"))
                    argState = ArgSpec;
                else if (arg == QLatin1String("-xplatform") || arg == QLatin1String("-xspec"))
                    argState = ArgXSpec;
                else if (arg == QLatin1String("-template") || arg == QLatin1String("-t"))
                    argState = ArgTmpl;
                else if (arg == QLatin1String("-template_prefix") || arg == QLatin1String("-tp"))
                    argState = ArgTmplPfx;
                else if (arg == QLatin1String("-win32"))
                    dir_sep = QLatin1Char('\\');
                else if (arg == QLatin1String("-unix"))
                    dir_sep = QLatin1Char('/');
                else
                    return ArgumentUnknown;
            } else if (arg.contains(QLatin1Char('='))) {
                state.cmds[state.phase] << arg;
            } else {
                return ArgumentUnknown;
            }
            continue;
        }
        argState = ArgNone;
    }
    if (argState != ArgNone)
        return ArgumentMalformed;
    return ArgumentsOk;
}

void QMakeGlobals::commitCommandLineArguments(QMakeCmdLineParserState &state)
{
    if (!state.extraargs.isEmpty()) {
        QString extra = fL1S("QMAKE_EXTRA_ARGS =");
        for (const QString &ea : std::as_const(state.extraargs))
            extra += QLatin1Char(' ') + QMakeEvaluator::quoteValue(ProString(ea));
        state.cmds[QMakeEvalBefore] << extra;
    }
    for (int p = 0; p < 4; p++) {
        if (!state.configs[p].isEmpty())
            state.cmds[p] << (fL1S("CONFIG += ") + state.configs[p].join(QLatin1Char(' ')));
        extra_cmds[p] = state.cmds[p].join(QLatin1Char('\n'));
    }

    if (xqmakespec.isEmpty())
        xqmakespec = qmakespec;
}

void QMakeGlobals::useEnvironment()
{
    if (xqmakespec.isEmpty())
        xqmakespec = getEnv(QLatin1String("XQMAKESPEC"));
    if (qmakespec.isEmpty()) {
        qmakespec = getEnv(QLatin1String("QMAKESPEC"));
        if (xqmakespec.isEmpty())
            xqmakespec = qmakespec;
    }
}

void QMakeGlobals::setCommandLineArguments(const QString &pwd, const QStringList &_args)
{
    QStringList args = _args;

    QMakeCmdLineParserState state(pwd);
    for (int pos = 0; pos < args.size(); pos++)
        addCommandLineArguments(state, args, &pos);
    commitCommandLineArguments(state);
    useEnvironment();
}

void QMakeGlobals::setDirectories(const QString &input_dir, const QString &output_dir)
{
    if (input_dir != output_dir && !output_dir.isEmpty()) {
        QString srcpath = input_dir;
        if (!srcpath.endsWith(QLatin1Char('/')))
            srcpath += QLatin1Char('/');
        QString dstpath = output_dir;
        if (!dstpath.endsWith(QLatin1Char('/')))
            dstpath += QLatin1Char('/');
        int srcLen = srcpath.size();
        int dstLen = dstpath.size();
        int lastSl = -1;
        while (++lastSl, --srcLen, --dstLen,
               srcLen && dstLen && srcpath.at(srcLen) == dstpath.at(dstLen))
            if (srcpath.at(srcLen) == QLatin1Char('/'))
                lastSl = 0;
        source_root = srcpath.left(srcLen + lastSl);
        build_root = dstpath.left(dstLen + lastSl);
    }
}

QString QMakeGlobals::shadowedPath(const QString &fileName) const
{
    if (source_root.isEmpty())
        return fileName;
    if (fileName.startsWith(source_root)
        && (fileName.size() == source_root.size()
            || fileName.at(source_root.size()) == QLatin1Char('/'))) {
        return build_root + fileName.mid(source_root.size());
    }
    return QString();
}

QStringList QMakeGlobals::splitPathList(const QString &val) const
{
    QStringList ret;
    if (!val.isEmpty()) {
        QString cwd(QDir::currentPath());
        const QStringList vals = val.split(dirlist_sep, Qt::SkipEmptyParts);
        ret.reserve(vals.size());
        for (const QString &it : vals)
            ret << IoUtils::resolvePath(cwd, it);
    }
    return ret;
}

QString QMakeGlobals::getEnv(const QString &var) const
{
#ifdef PROEVALUATOR_SETENV
    return environment.value(var);
#else
    return QString::fromLocal8Bit(qgetenv(var.toLocal8Bit().constData()));
#endif
}

QStringList QMakeGlobals::getPathListEnv(const QString &var) const
{
    return splitPathList(getEnv(var));
}

QString QMakeGlobals::expandEnvVars(const QString &str) const
{
    QString string = str;
    int startIndex = 0;
    forever {
        startIndex = string.indexOf(QLatin1Char('$'), startIndex);
        if (startIndex < 0)
            break;
        if (string.size() < startIndex + 3)
            break;
        if (string.at(startIndex + 1) != QLatin1Char('(')) {
            startIndex++;
            continue;
        }
        int endIndex = string.indexOf(QLatin1Char(')'), startIndex + 2);
        if (endIndex < 0)
            break;
        QString value = getEnv(string.mid(startIndex + 2, endIndex - startIndex - 2));
        string.replace(startIndex, endIndex - startIndex + 1, value);
        startIndex += value.size();
    }
    return string;
}

#ifndef QT_BUILD_QMAKE
#ifdef PROEVALUATOR_INIT_PROPS
bool QMakeGlobals::initProperties()
{
    QByteArray data;
#if QT_CONFIG(process)
    QProcess proc;
    proc.start(qmake_abslocation, QStringList() << QLatin1String("-query"));
    if (!proc.waitForFinished())
        return false;
    data = proc.readAll();
#else
    if (FILE *proc = QT_POPEN(QString(IoUtils::shellQuote(qmake_abslocation)
                                      + QLatin1String(" -query")).toLocal8Bit(), QT_POPEN_READ)) {
        char buff[1024];
        while (!feof(proc))
            data.append(buff, int(fread(buff, 1, 1023, proc)));
        QT_PCLOSE(proc);
    }
#endif
    parseProperties(data, properties);
    return true;
}
#endif

void QMakeGlobals::parseProperties(const QByteArray &data, QHash<ProKey, ProString> &properties)
{
    const auto lines = data.split('\n');
    for (QByteArray line : lines) {
        int off = line.indexOf(':');
        if (off < 0) // huh?
            continue;
        if (line.endsWith('\r'))
            line.chop(1);
        QString name = QString::fromLatin1(line.left(off));
        ProString value = ProString(QDir::fromNativeSeparators(
                    QString::fromLocal8Bit(line.mid(off + 1))));
        if (value.isNull())
            value = ProString(""); // Make sure it is not null, to discern from missing keys
        properties.insert(ProKey(name), value);
        if (name.startsWith(QLatin1String("QT_"))) {
            enum { PropPut, PropRaw, PropGet } variant;
            if (name.contains(QLatin1Char('/'))) {
                if (name.endsWith(QLatin1String("/raw")))
                    variant = PropRaw;
                else if (name.endsWith(QLatin1String("/get")))
                    variant = PropGet;
                else  // Nothing falls back on /src or /dev.
                    continue;
                name.chop(4);
            } else {
                variant = PropPut;
            }
            if (name.startsWith(QLatin1String("QT_INSTALL_"))) {
                if (variant < PropRaw) {
                    if (name == QLatin1String("QT_INSTALL_PREFIX")
                        || name == QLatin1String("QT_INSTALL_DATA")
                        || name == QLatin1String("QT_INSTALL_LIBS")
                        || name == QLatin1String("QT_INSTALL_BINS")) {
                        // Qt4 fallback
                        QString hname = name;
                        hname.replace(3, 7, QLatin1String("HOST"));
                        properties.insert(ProKey(hname), value);
                        properties.insert(ProKey(hname + QLatin1String("/get")), value);
                        properties.insert(ProKey(hname + QLatin1String("/src")), value);
                    }
                    properties.insert(ProKey(name + QLatin1String("/raw")), value);
                }
                if (variant <= PropRaw)
                    properties.insert(ProKey(name + QLatin1String("/dev")), value);
            } else if (!name.startsWith(QLatin1String("QT_HOST_"))) {
                continue;
            }
            if (variant != PropRaw) {
                if (variant < PropGet)
                    properties.insert(ProKey(name + QLatin1String("/get")), value);
                properties.insert(ProKey(name + QLatin1String("/src")), value);
            }
        }
    }
}
#endif // QT_BUILD_QMAKE

QT_END_NAMESPACE