/*
 * Bittorrent Client using Qt and libtorrent.
 * Copyright (C) 2016  Eugene Shalygin <eugene.shalygin@gmail.com>
 * Copyright (C) 2014  Vladimir Golovnev <glassez@yandex.ru>
 * Copyright (C) 2006  Christophe Dumez
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * In addition, as a special exception, the copyright holders give permission to
 * link this program with the OpenSSL project's "OpenSSL" library (or with
 * modified versions of it that use the same license as the "OpenSSL" library),
 * and distribute the linked executables. You must obey the GNU General Public
 * License in all respects for all of the code used other than "OpenSSL".  If you
 * modify file(s), you may extend this exception to your version of the file(s),
 * but you are not obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 *
 * Contact : chris@qbittorrent.org
 */

#include "cmdoptions.h"

#include <iostream>

#include <QDebug>
#include <QFileInfo>
#include <QProcessEnvironment>
#include <QTextStream>

#ifdef Q_OS_WIN
#include <QMessageBox>
#endif

#include "base/utils/misc.h"
#include "base/utils/string.h"

namespace
{
    const int USAGE_INDENTATION = 4;
    const int USAGE_TEXT_COLUMN = 31;
    const int WRAP_AT_COLUMN = 80;

    // Base option class. Encapsulates name operations.
    class Option
    {
    protected:
        constexpr Option(const char *name, char shortcut = 0)
            : m_name {name}
            , m_shortcut {shortcut}
        {
        }

        QString fullParameter() const
        {
            return QLatin1String("--") + QLatin1String(m_name);
        }

        QString shortcutParameter() const
        {
            return QLatin1String("-") + QLatin1Char(m_shortcut);
        }

        bool hasShortcut() const
        {
            return m_shortcut != 0;
        }

        QString envVarName() const
        {
            return QLatin1String("QBT_")
                   + QString(QLatin1String(m_name)).toUpper().replace(QLatin1Char('-'), QLatin1Char('_'));
        }

    public:
        static QString padUsageText(const QString &usage)
        {
            QString res = QString(USAGE_INDENTATION, ' ') + usage;

            if ((USAGE_TEXT_COLUMN - usage.length() - 4) > 0)
                return res + QString(USAGE_TEXT_COLUMN - usage.length() - 4, ' ');
            else
                return res;
        }

    private:
        const char *m_name;
        const char m_shortcut;
    };

    // Boolean option.
    class BoolOption: protected Option
    {
    public:
        constexpr BoolOption(const char *name, char shortcut = 0)
            : Option {name, shortcut}
        {
        }

        bool operator==(const QString &arg) const
        {
            return (hasShortcut() && ((arg.size() == 2) && (arg == shortcutParameter())))
                   || (arg == fullParameter());
        }

        bool value(const QProcessEnvironment &env) const
        {
            QString val = env.value(envVarName());
            // we accept "1" and "true" (upper or lower cased) as boolean 'true' values
            return (val == QLatin1String("1") || val.toUpper() == QLatin1String("TRUE"));
        }

        QString usage() const
        {
            QString res;
            if (hasShortcut())
                res += shortcutParameter() + QLatin1String(" | ");
            res += fullParameter();
            return padUsageText(res);
        }
    };

    bool operator==(const QString &s, const BoolOption &o)
    {
        return o == s;
    }

    // Option with string value. May not have a shortcut
    struct StringOption: protected Option
    {
    public:
        constexpr StringOption(const char *name)
            : Option {name, 0}
        {
        }

        bool operator==(const QString &arg) const
        {
            return arg.startsWith(parameterAssignment());
        }

        QString value(const QString &arg) const
        {
            QStringList parts = arg.split(QLatin1Char('='));
            if (parts.size() == 2)
                return Utils::String::unquote(parts[1], QLatin1String("'\""));
            throw CommandLineParameterError(QObject::tr("Parameter '%1' must follow syntax '%1=%2'",
                                                        "e.g. Parameter '--webui-port' must follow syntax '--webui-port=value'")
                                            .arg(fullParameter()).arg(QLatin1String("<value>")));
        }

        QString value(const QProcessEnvironment &env, const QString &defaultValue = QString()) const
        {
            QString val = env.value(envVarName());
            return val.isEmpty() ? defaultValue : Utils::String::unquote(val, QLatin1String("'\""));
        }

        QString usage(const QString &valueName) const
        {
            return padUsageText(parameterAssignment() + QLatin1Char('<') + valueName + QLatin1Char('>'));
        }

    private:
        QString parameterAssignment() const
        {
            return fullParameter() + QLatin1Char('=');
        }
    };

    bool operator==(const QString &s, const StringOption &o)
    {
        return o == s;
    }

    // Option with integer value. May not have a shortcut
    class IntOption: protected StringOption
    {
    public:
        constexpr IntOption(const char *name)
            : StringOption {name}
        {
        }

        using StringOption::operator==;
        using StringOption::usage;

        int value(const QString &arg) const
        {
            QString val = StringOption::value(arg);
            bool ok = false;
            int res = val.toInt(&ok);
            if (!ok)
                throw CommandLineParameterError(QObject::tr("Parameter '%1' must follow syntax '%1=%2'",
                                                            "e.g. Parameter '--webui-port' must follow syntax '--webui-port=<value>'")
                                                .arg(fullParameter()).arg(QLatin1String("<integer value>")));
            return res;
        }

        int value(const QProcessEnvironment &env, int defaultValue) const
        {
            QString val = env.value(envVarName());
            if (val.isEmpty()) return defaultValue;

            bool ok;
            int res = val.toInt(&ok);
            if (!ok) {
                qDebug() << QObject::tr("Expected integer number in environment variable '%1', but got '%2'")
                    .arg(envVarName()).arg(val);
                return defaultValue;
            }
            return res;
        }
    };

    bool operator==(const QString &s, const IntOption &o)
    {
        return o == s;
    }

    // Option that is explicitly set to true or false, and whose value is undefined when unspecified.
    // May not have a shortcut.
    class TriStateBoolOption: protected Option
    {
    public:
        constexpr TriStateBoolOption(const char *name, bool defaultValue)
            : Option {name, 0}
            , m_defaultValue(defaultValue)
        {
        }

        bool operator==(const QString &arg) const
        {
            QStringList parts = arg.split(QLatin1Char('='));
            return parts[0] == fullParameter();
        }

        QString usage() const
        {
            return padUsageText(fullParameter() + QLatin1String("=<true|false>"));
        }

        TriStateBool value(const QString &arg) const
        {
            QStringList parts = arg.split(QLatin1Char('='));

            if (parts.size() == 1) {
                return TriStateBool(m_defaultValue);
            }
            else if (parts.size() == 2) {
                QString val = parts[1];

                if (val.toUpper() == QLatin1String("TRUE") || val == QLatin1String("1")) {
                    return TriStateBool::True;
                }
                else if (val.toUpper() == QLatin1String("FALSE") || val == QLatin1String("0")) {
                    return TriStateBool::False;
                }
            }

            throw CommandLineParameterError(QObject::tr("Parameter '%1' must follow syntax '%1=%2'",
                                                        "e.g. Parameter '--add-paused' must follow syntax "
                                                        "'--add-paused=<true|false>'")
                                            .arg(fullParameter())
                                            .arg(QLatin1String("<true|false>")));
        }

        TriStateBool value(const QProcessEnvironment &env) const
        {
            QString val = env.value(envVarName(), "-1");

            if (val.isEmpty()) {
                return TriStateBool(m_defaultValue);
            }
            else if (val == QLatin1String("-1")) {
                return TriStateBool::Undefined;
            }
            else if (val.toUpper() == QLatin1String("TRUE") || val == QLatin1String("1")) {
                return TriStateBool::True;
            }
            else if (val.toUpper() == QLatin1String("FALSE") || val == QLatin1String("0")) {
                return TriStateBool::False;
            }
            else {
                qDebug() << QObject::tr("Expected %1 in environment variable '%2', but got '%3'")
                    .arg(QLatin1String("true|false")).arg(envVarName()).arg(val);
                return TriStateBool::Undefined;
            }
        }

        bool m_defaultValue;
    };

    bool operator==(const QString &s, const TriStateBoolOption &o)
    {
        return o == s;
    }

    constexpr const BoolOption SHOW_HELP_OPTION = {"help", 'h'};
    constexpr const BoolOption SHOW_VERSION_OPTION = {"version", 'v'};
#ifdef DISABLE_GUI
    constexpr const BoolOption DAEMON_OPTION = {"daemon", 'd'};
#else
    constexpr const BoolOption NO_SPLASH_OPTION = {"no-splash"};
#endif
    constexpr const IntOption WEBUI_PORT_OPTION = {"webui-port"};
    constexpr const StringOption PROFILE_OPTION = {"profile"};
    constexpr const StringOption CONFIGURATION_OPTION = {"configuration"};
    constexpr const BoolOption PORTABLE_OPTION = {"portable"};
    constexpr const BoolOption RELATIVE_FASTRESUME = {"relative-fastresume"};
    constexpr const StringOption SAVE_PATH_OPTION = {"save-path"};
    constexpr const TriStateBoolOption PAUSED_OPTION = {"add-paused", true};
    constexpr const BoolOption SKIP_HASH_CHECK_OPTION = {"skip-hash-check"};
    constexpr const StringOption CATEGORY_OPTION = {"category"};
    constexpr const BoolOption SEQUENTIAL_OPTION = {"sequential"};
    constexpr const BoolOption FIRST_AND_LAST_OPTION = {"first-and-last"};
    constexpr const TriStateBoolOption SKIP_DIALOG_OPTION = {"skip-dialog", true};
}

QBtCommandLineParameters::QBtCommandLineParameters(const QProcessEnvironment &env)
    : showHelp(false)
    , relativeFastresumePaths(RELATIVE_FASTRESUME.value(env))
    , portableMode(PORTABLE_OPTION.value(env))
    , skipChecking(SKIP_HASH_CHECK_OPTION.value(env))
    , sequential(SEQUENTIAL_OPTION.value(env))
    , firstLastPiecePriority(FIRST_AND_LAST_OPTION.value(env))
#ifndef Q_OS_WIN
    , showVersion(false)
#endif
#ifndef DISABLE_GUI
    , noSplash(NO_SPLASH_OPTION.value(env))
#else
    , shouldDaemonize(DAEMON_OPTION.value(env))
#endif
    , webUiPort(WEBUI_PORT_OPTION.value(env, -1))
    , addPaused(PAUSED_OPTION.value(env))
    , skipDialog(SKIP_DIALOG_OPTION.value(env))
    , profileDir(PROFILE_OPTION.value(env))
    , configurationName(CONFIGURATION_OPTION.value(env))
    , savePath(SAVE_PATH_OPTION.value(env))
    , category(CATEGORY_OPTION.value(env))
{
}

QStringList QBtCommandLineParameters::paramList() const
{
    QStringList result;
    // Because we're passing a string list to the currently running
    // qBittorrent process, we need some way of passing along the options
    // the user has specified. Here we place special strings that are
    // almost certainly not going to collide with a file path or URL
    // specified by the user, and placing them at the beginning of the
    // string listr so that they will be processed before the list of
    // torrent paths or URLs.

    if (!savePath.isEmpty())
        result.append(QString("@savePath=%1").arg(savePath));

    if (addPaused == TriStateBool::True) {
        result.append(QLatin1String("@addPaused=1"));
    }
    else if (addPaused == TriStateBool::False) {
        result.append(QLatin1String("@addPaused=0"));
    }

    if (skipChecking)
        result.append(QLatin1String("@skipChecking"));

    if (!category.isEmpty())
        result.append(QString("@category=%1").arg(category));

    if (sequential)
        result.append(QLatin1String("@sequential"));

    if (firstLastPiecePriority)
        result.append(QLatin1String("@firstLastPiecePriority"));

    if (skipDialog == TriStateBool::True) {
        result.append(QLatin1String("@skipDialog=1"));
    }
    else if (skipDialog == TriStateBool::False) {
        result.append(QLatin1String("@skipDialog=0"));
    }

    result += torrents;
    return result;
}

QBtCommandLineParameters parseCommandLine(const QStringList &args)
{
    QBtCommandLineParameters result {QProcessEnvironment::systemEnvironment()};

    for (int i = 1; i < args.count(); ++i) {
        const QString &arg = args[i];

        if ((arg.startsWith("--") && !arg.endsWith(".torrent"))
            || (arg.startsWith("-") && (arg.size() == 2))) {
            // Parse known parameters
            if ((arg == SHOW_HELP_OPTION)) {
                result.showHelp = true;
            }
#ifndef Q_OS_WIN
            else if (arg == SHOW_VERSION_OPTION) {
                result.showVersion = true;
            }
#endif
            else if (arg == WEBUI_PORT_OPTION) {
                result.webUiPort = WEBUI_PORT_OPTION.value(arg);
                if ((result.webUiPort < 1) || (result.webUiPort > 65535))
                    throw CommandLineParameterError(QObject::tr("%1 must specify a valid port (1 to 65535).")
                                                    .arg(QLatin1String("--webui-port")));
            }
#ifndef DISABLE_GUI
            else if (arg == NO_SPLASH_OPTION) {
                result.noSplash = true;
            }
#else
            else if (arg == DAEMON_OPTION) {
                result.shouldDaemonize = true;
            }
#endif
            else if (arg == PROFILE_OPTION) {
                result.profileDir = PROFILE_OPTION.value(arg);
            }
            else if (arg == RELATIVE_FASTRESUME) {
                result.relativeFastresumePaths = true;
            }
            else if (arg == PORTABLE_OPTION) {
                result.portableMode = true;
            }
            else if (arg == CONFIGURATION_OPTION) {
                result.configurationName = CONFIGURATION_OPTION.value(arg);
            }
            else if (arg == SAVE_PATH_OPTION) {
                result.savePath = SAVE_PATH_OPTION.value(arg);
            }
            else if (arg == PAUSED_OPTION) {
                result.addPaused = PAUSED_OPTION.value(arg);
            }
            else if (arg == SKIP_HASH_CHECK_OPTION) {
                result.skipChecking = true;
            }
            else if (arg == CATEGORY_OPTION) {
                result.category = CATEGORY_OPTION.value(arg);
            }
            else if (arg == SEQUENTIAL_OPTION) {
                result.sequential = true;
            }
            else if (arg == FIRST_AND_LAST_OPTION) {
                result.firstLastPiecePriority = true;
            }
            else if (arg == SKIP_DIALOG_OPTION) {
                result.skipDialog = SKIP_DIALOG_OPTION.value(arg);
            }
            else {
                // Unknown argument
                result.unknownParameter = arg;
                break;
            }
        }
        else {
            QFileInfo torrentPath;
            torrentPath.setFile(arg);

            if (torrentPath.exists())
                result.torrents += torrentPath.absoluteFilePath();
            else
                result.torrents += arg;
        }
    }

    return result;
}

CommandLineParameterError::CommandLineParameterError(const QString &messageForUser)
    : std::runtime_error(messageForUser.toLocal8Bit().data())
    , m_messageForUser(messageForUser)
{
}

const QString& CommandLineParameterError::messageForUser() const
{
    return m_messageForUser;
}

QString wrapText(const QString &text, int initialIndentation = USAGE_TEXT_COLUMN, int wrapAtColumn = WRAP_AT_COLUMN)
{
    QStringList words = text.split(' ');
    QStringList lines = {words.first()};
    int currentLineMaxLength = wrapAtColumn - initialIndentation;

    foreach (const QString &word, words.mid(1)) {
        if (lines.last().length() + word.length() + 1 < currentLineMaxLength) {
            lines.last().append(" " + word);
        }
        else {
            lines.append(QString(initialIndentation, ' ') + word);
            currentLineMaxLength = wrapAtColumn;
        }
    }

    return lines.join("\n");
}

QString makeUsage(const QString &prgName)
{
    QString text;
    QTextStream stream(&text, QIODevice::WriteOnly);
    QString indentation = QString(USAGE_INDENTATION, ' ');

    stream << QObject::tr("Usage:") << '\n';
    stream << indentation << prgName << QLatin1String(" [options] [(<filename> | <url>)...]") << '\n';

    stream << QObject::tr("Options:") << '\n';
#ifndef Q_OS_WIN
    stream << SHOW_VERSION_OPTION.usage() << wrapText(QObject::tr("Display program version and exit")) << '\n';
#endif
    stream << SHOW_HELP_OPTION.usage() << wrapText(QObject::tr("Display this help message and exit")) << '\n';
    stream << WEBUI_PORT_OPTION.usage(QObject::tr("port"))
           << wrapText(QObject::tr("Change the Web UI port"))
           << '\n';
#ifndef DISABLE_GUI
    stream << NO_SPLASH_OPTION.usage() << wrapText(QObject::tr("Disable splash screen")) << '\n';
#else
    stream << DAEMON_OPTION.usage() << wrapText(QObject::tr("Run in daemon-mode (background)")) << '\n';
#endif
    //: Use appropriate short form or abbreviation of "directory"
    stream << PROFILE_OPTION.usage(QObject::tr("dir"))
           << wrapText(QObject::tr("Store configuration files in <dir>")) << '\n';
    stream << CONFIGURATION_OPTION.usage(QObject::tr("name"))
           << wrapText(QObject::tr("Store configuration files in directories qBittorrent_<name>")) << '\n';
    stream << RELATIVE_FASTRESUME.usage()
           << wrapText(QObject::tr("Hack into libtorrent fastresume files and make file paths relative "
                                         "to the profile directory")) << '\n';
    stream << PORTABLE_OPTION.usage()
           << wrapText(QObject::tr("Shortcut for %1", "Shortcut for --profile=<exe dir>/profile --relative-fastresume")
                       .arg(QLatin1String("--profile=<exe dir>/profile --relative-fastresume"))) << '\n';
    stream << Option::padUsageText(QObject::tr("files or URLs"))
           << wrapText(QObject::tr("Download the torrents passed by the user")) << '\n'
           << '\n';

    stream << wrapText(QObject::tr("Options when adding new torrents:"), 0) << '\n';
    stream << SAVE_PATH_OPTION.usage(QObject::tr("path")) << wrapText(QObject::tr("Torrent save path")) << '\n';
    stream << PAUSED_OPTION.usage() << wrapText(QObject::tr("Add torrents as started or paused")) << '\n';
    stream << SKIP_HASH_CHECK_OPTION.usage() << wrapText(QObject::tr("Skip hash check")) << '\n';
    stream << CATEGORY_OPTION.usage(QObject::tr("name"))
           << wrapText(QObject::tr("Assign torrents to category. If the category doesn't exist, it will be "
                                   "created.")) << '\n';
    stream << SEQUENTIAL_OPTION.usage() << wrapText(QObject::tr("Download files in sequential order")) << '\n';
    stream << FIRST_AND_LAST_OPTION.usage()
           << wrapText(QObject::tr("Download first and last pieces first")) << '\n';
    stream << SKIP_DIALOG_OPTION.usage()
           << wrapText(QObject::tr("Specify whether the \"Add New Torrent\" dialog opens when adding a "
                                   "torrent.")) << '\n';
    stream << '\n';

    stream << wrapText(QObject::tr("Option values may be supplied via environment variables. For option named "
                                   "'parameter-name', environment variable name is 'QBT_PARAMETER_NAME' (in upper "
                                   "case, '-' replaced with '_'). To pass flag values, set the variable to '1' or "
                                   "'TRUE'. For example, to disable the splash screen: "), 0) << "\n"
           << QLatin1String("QBT_NO_SPLASH=1 ") << prgName << '\n'
           << wrapText(QObject::tr("Command line parameters take precedence over environment variables"), 0) << '\n';

    stream << flush;
    return text;
}

void displayUsage(const QString &prgName)
{
#ifndef Q_OS_WIN
    std::cout << qPrintable(makeUsage(prgName)) << std::endl;
#else
    QMessageBox msgBox(QMessageBox::Information, QObject::tr("Help"), makeUsage(prgName), QMessageBox::Ok);
    msgBox.show(); // Need to be shown or to moveToCenter does not work
    msgBox.move(Utils::Misc::screenCenter(&msgBox));
    msgBox.exec();
#endif
}
