#include "gamelauncher.h"

#include "javalocator.h"

#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHash>
#include <QProcess>
#include <QtCore/private/qzipreader_p.h>

namespace {

#if defined(Q_OS_WIN)
constexpr QChar kClasspathSeparator = u';';
#else
constexpr QChar kClasspathSeparator = u':';
#endif

QString libraryPath(const QString &gameDir, const MinecraftLibrary &library)
{
    return gameDir + QStringLiteral("/libraries/") + library.path;
}

QString nativeToken(const QString &path)
{
    return QDir::toNativeSeparators(path);
}

// Подставляет ${...}. Аргументы, в которых после подстановки остались
// неизвестные плейсхолдеры, вызывающий выбрасывает.
QString substitute(QString argument, const QHash<QString, QString> &variables)
{
    for (auto it = variables.cbegin(); it != variables.cend(); ++it)
        argument.replace(QStringLiteral("${") + it.key() + u'}', it.value());
    return argument;
}

bool isNativeLibraryFile(const QString &fileName)
{
    static const QStringList suffixes = {QStringLiteral(".dll"),
                                         QStringLiteral(".so"),
                                         QStringLiteral(".dylib"),
                                         QStringLiteral(".jnilib")};
    for (const QString &suffix : suffixes)
        if (fileName.endsWith(suffix, Qt::CaseInsensitive))
            return true;
    return fileName.contains(QStringLiteral(".so."));   // libfoo.so.1
}

} // namespace

GameLauncher::GameLauncher(QObject *parent)
    : QObject(parent)
{
}

bool GameLauncher::isRunning() const
{
    return m_process && m_process->state() != QProcess::NotRunning;
}

QStringList GameLauncher::missingFiles(const LaunchOptions &options,
                                       const MinecraftVersion &version,
                                       int limit)
{
    QStringList missing;
    const auto add = [&missing, limit](const QString &path) {
        if (missing.size() < limit)
            missing << QDir::toNativeSeparators(path);
    };

    if (!QDir(options.gameDir).exists())
        return {QDir::toNativeSeparators(options.gameDir)};

    if (!QFile::exists(version.clientJarPath))
        add(version.clientJarPath);

    for (const MinecraftLibrary &library : version.libraries) {
        const QString path = libraryPath(options.gameDir, library);
        if (!QFile::exists(path))
            add(path);
    }

    if (!version.assetIndexId.isEmpty()) {
        const QString index = options.gameDir + QStringLiteral("/assets/indexes/")
                              + version.assetIndexId + QStringLiteral(".json");
        if (!QFile::exists(index))
            add(index);
        else if (!QDir(options.gameDir + QStringLiteral("/assets/objects")).exists())
            add(options.gameDir + QStringLiteral("/assets/objects"));
    }

    return missing;
}

QStringList GameLauncher::buildArguments(const LaunchOptions &options,
                                         const MinecraftVersion &version,
                                         const QString &nativesDir)
{
    QStringList classpath;
    for (const MinecraftLibrary &library : version.libraries) {
        if (library.native)
            continue;   // натив-библиотеки только распаковываются
        classpath << nativeToken(libraryPath(options.gameDir, library));
    }
    classpath << nativeToken(version.clientJarPath);

    const QString assetsRoot = options.gameDir + QStringLiteral("/assets");
    const bool legacyAssets = version.assetsKind == QLatin1String("legacy")
                              || version.assetsKind == QLatin1String("pre-1.6");

    QHash<QString, QString> variables;
    variables[QStringLiteral("auth_player_name")] = options.playerName;
    variables[QStringLiteral("auth_uuid")] = options.uuid;
    variables[QStringLiteral("auth_access_token")] = options.accessToken;
    variables[QStringLiteral("auth_session")] =
        QStringLiteral("token:%1:%2").arg(options.accessToken, options.uuid);
    variables[QStringLiteral("auth_xuid")] = QStringLiteral("0");
    variables[QStringLiteral("clientid")] =
        options.clientToken.isEmpty() ? QStringLiteral("0") : options.clientToken;
    variables[QStringLiteral("user_type")] = options.userType;
    variables[QStringLiteral("user_properties")] = QStringLiteral("{}");
    variables[QStringLiteral("version_name")] = version.id;
    variables[QStringLiteral("version_type")] = version.type;
    variables[QStringLiteral("game_directory")] = nativeToken(options.gameDir);
    variables[QStringLiteral("assets_root")] = nativeToken(assetsRoot);
    variables[QStringLiteral("assets_index_name")] = version.assetIndexId;
    variables[QStringLiteral("game_assets")] =
        nativeToken(legacyAssets ? assetsRoot + QStringLiteral("/virtual/legacy") : assetsRoot);
    variables[QStringLiteral("natives_directory")] = nativeToken(nativesDir);
    variables[QStringLiteral("library_directory")] =
        nativeToken(options.gameDir + QStringLiteral("/libraries"));
    variables[QStringLiteral("classpath")] = classpath.join(kClasspathSeparator);
    variables[QStringLiteral("classpath_separator")] = QString(kClasspathSeparator);
    variables[QStringLiteral("primary_jar")] = nativeToken(version.clientJarPath);
    variables[QStringLiteral("launcher_name")] = options.launcherName;
    variables[QStringLiteral("launcher_version")] = options.launcherVersion;
    variables[QStringLiteral("resolution_width")] = QString::number(options.windowWidth);
    variables[QStringLiteral("resolution_height")] = QString::number(options.windowHeight);

    QStringList arguments;

    // Свои jvm-аргументы идут первыми — как в строке запуска из примера.
    if (!options.authlibInjectorPath.isEmpty()) {
        arguments << QStringLiteral("-javaagent:%1=%2")
                         .arg(nativeToken(options.authlibInjectorPath), options.authlibInjectorApi)
                  << QStringLiteral("-Dauthlibinjector.side=client");
    }
    if (options.minMemoryMb > 0)
        arguments << QStringLiteral("-Xms%1M").arg(options.minMemoryMb);
    if (options.maxMemoryMb > 0)
        arguments << QStringLiteral("-Xmx%1M").arg(options.maxMemoryMb);
    arguments += options.extraJvmArgs;

    for (const QString &argument : version.jvmArgs) {
        const QString resolved = substitute(argument, variables);
        if (!resolved.contains(QStringLiteral("${")))
            arguments << resolved;
    }

    if (!version.loggingArgument.isEmpty() && QFile::exists(version.loggingConfigPath)) {
        QHash<QString, QString> loggingVariable;
        loggingVariable[QStringLiteral("path")] = nativeToken(version.loggingConfigPath);
        arguments << substitute(version.loggingArgument, loggingVariable);
    }

    arguments << version.mainClass;

    for (const QString &argument : version.gameArgs) {
        const QString resolved = substitute(argument, variables);
        if (!resolved.contains(QStringLiteral("${")))
            arguments << resolved;
    }

    // Разрешение окна: у старых версий фич-флага нет, аргументы всё равно понимаются.
    if (options.windowWidth > 0 && options.windowHeight > 0
        && !version.hasCustomResolutionArgs && !arguments.contains(QStringLiteral("--width"))) {
        arguments << QStringLiteral("--width") << QString::number(options.windowWidth)
                  << QStringLiteral("--height") << QString::number(options.windowHeight);
    }
    if (options.fullscreen && !arguments.contains(QStringLiteral("--fullscreen")))
        arguments << QStringLiteral("--fullscreen");

    if (!options.serverAddress.isEmpty()) {
        if (version.supportsQuickPlay) {
            arguments << QStringLiteral("--quickPlayMultiplayer") << options.serverAddress;
        } else {
            const int colon = options.serverAddress.lastIndexOf(u':');
            const QString host = colon > 0 ? options.serverAddress.left(colon) : options.serverAddress;
            const QString port = colon > 0 ? options.serverAddress.mid(colon + 1)
                                           : QStringLiteral("25565");
            arguments << QStringLiteral("--server") << host << QStringLiteral("--port") << port;
        }
    }

    return arguments;
}

bool GameLauncher::extractNatives(const LaunchOptions &options,
                                  const MinecraftVersion &version,
                                  const QString &nativesDir,
                                  QString *error)
{
    if (!QDir().mkpath(nativesDir)) {
        if (error)
            *error = tr("Не удалось создать папку %1").arg(QDir::toNativeSeparators(nativesDir));
        return false;
    }

    for (const MinecraftLibrary &library : version.libraries) {
        if (!library.native)
            continue;

        const QString jar = libraryPath(options.gameDir, library);
        QZipReader reader(jar);
        if (!reader.isReadable()) {
            if (error)
                *error = tr("Не удалось прочитать натив-библиотеку %1")
                             .arg(QDir::toNativeSeparators(jar));
            return false;
        }

        const QList<QZipReader::FileInfo> entries = reader.fileInfoList();
        for (const QZipReader::FileInfo &entry : entries) {
            if (!entry.isFile)
                continue;
            if (entry.filePath.startsWith(QStringLiteral("META-INF/")))
                continue;

            bool excluded = false;
            for (const QString &prefix : library.extractExclude) {
                if (entry.filePath.startsWith(prefix)) {
                    excluded = true;
                    break;
                }
            }
            if (excluded)
                continue;

            // java.library.path ищет библиотеки только в корне папки, поэтому
            // складываем их плоско, без структуры каталогов внутри jar.
            const QString fileName = QFileInfo(entry.filePath).fileName();
            if (!isNativeLibraryFile(fileName))
                continue;

            const QString target = nativesDir + u'/' + fileName;
            const QByteArray data = reader.fileData(entry.filePath);
            if (QFileInfo(target).size() == data.size() && QFile::exists(target))
                continue;   // уже распаковано этим же лаунчером

            QFile file(target);
            if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) {
                if (error)
                    *error = tr("Не удалось распаковать %1 в %2")
                                 .arg(fileName, QDir::toNativeSeparators(nativesDir));
                return false;
            }
        }
    }

    return true;
}

bool GameLauncher::launch(const LaunchOptions &options,
                          const MinecraftVersion &version,
                          QString *error)
{
    if (isRunning()) {
        if (error)
            *error = tr("Игра уже запущена");
        return false;
    }

    emit progress(tr("Проверка файлов игры…"));
    const QStringList missing = missingFiles(options, version);
    if (!missing.isEmpty()) {
        if (error)
            *error = tr("В .minecraft не хватает файлов (%1):\n%2")
                         .arg(missing.size())
                         .arg(missing.join(u'\n'));
        return false;
    }

    emit progress(tr("Распаковка нативных библиотек…"));
    const QString nativesDir = options.gameDir + QStringLiteral("/versions/") + version.id
                               + QStringLiteral("/natives");
    if (!extractNatives(options, version, nativesDir, error))
        return false;

    emit progress(tr("Поиск Java %1…").arg(version.javaMajor));
    const QString java = JavaLocator::select(options.gameDir, version.javaMajor, options.javaPath, error);
    if (java.isEmpty())
        return false;

    const QStringList arguments = buildArguments(options, version, nativesDir);
    const QString executable = JavaLocator::windowlessVariant(java);

    m_process = new QProcess(this);
    m_process->setProgram(executable);
    m_process->setArguments(arguments);
    m_process->setWorkingDirectory(options.gameDir);
    m_process->setProcessChannelMode(QProcess::MergedChannels);

    connect(m_process, &QProcess::readyReadStandardOutput, this, [this]() {
        const QString chunk = QString::fromLocal8Bit(m_process->readAllStandardOutput());
        const QStringList lines = chunk.split(u'\n', Qt::SkipEmptyParts);
        for (const QString &line : lines)
            emit output(line.trimmed());
    });
    connect(m_process, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
        emit gameFinished(exitCode, status == QProcess::CrashExit);
        m_process->deleteLater();
        m_process = nullptr;
    });
    // Токен в лог не попадает — маскируем его в отображаемой строке запуска.
    QStringList shown = arguments;
    const int tokenIndex = shown.indexOf(QStringLiteral("--accessToken"));
    if (tokenIndex >= 0 && tokenIndex + 1 < shown.size())
        shown[tokenIndex + 1] = QStringLiteral("<скрыт>");

    m_process->start();
    if (!m_process->waitForStarted(15000)) {
        if (error)
            *error = tr("Не удалось запустить %1: %2")
                         .arg(QDir::toNativeSeparators(executable), m_process->errorString());
        m_process->deleteLater();
        m_process = nullptr;
        return false;
    }

    emit gameStarted(QDir::toNativeSeparators(executable) + QStringLiteral(" ")
                     + shown.join(QStringLiteral(" ")));
    return true;
}

void GameLauncher::terminate()
{
    if (isRunning())
        m_process->kill();
}
