/*
 * Copyright (c) 2011-2021 Meltytech, LLC
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */

#include <QtWidgets>
#include <QtGlobal>
#include "mainwindow.h"
#include "settings.h"
#include <Logger.h>
#include <FileAppender.h>
#include <ConsoleAppender.h>
#include <QSysInfo>
#include <QProcess>
#include <QCommandLineParser>
#include <framework/mlt_log.h>
#include <QFile>
#include <QQuickStyle>

#ifdef Q_OS_MAC
    #include "macos.h"
#endif

#ifdef Q_OS_WIN
#ifdef QT_DEBUG
#   include <exchndl.h>
#endif
extern "C"
{
    // Inform the driver we could make use of the discrete gpu
    __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
    __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
#endif

static const int kMaxCacheCount = 5000;
#ifdef Q_OS_WIN
static const char* kDefaultScaleRoundPolicy = "RoundPreferFloor";
#else
static const char* kDefaultScaleRoundPolicy = "Round";
#endif

static void mlt_log_handler(void *service, int mlt_level, const char *format, va_list args)
{
    if (mlt_level > mlt_log_get_level())
        return;

    enum Logger::LogLevel cuteLoggerLevel = Logger::Fatal;
    switch (mlt_level) {
    case MLT_LOG_DEBUG:
        cuteLoggerLevel = Logger::Trace;
        break;
    case MLT_LOG_ERROR:
    case MLT_LOG_FATAL:
    case MLT_LOG_PANIC:
        cuteLoggerLevel = Logger::Error;
        break;
    case MLT_LOG_INFO:
        cuteLoggerLevel = Logger::Info;
        break;
    case MLT_LOG_VERBOSE:
        cuteLoggerLevel = Logger::Debug;
        break;
    case MLT_LOG_WARNING:
        cuteLoggerLevel = Logger::Warning;
        break;
    }
    QString message;
    mlt_properties properties = service? MLT_SERVICE_PROPERTIES((mlt_service) service) : NULL;
    if (properties) {
        char *mlt_type = mlt_properties_get(properties, "mlt_type");
        char *service_name = mlt_properties_get(properties, "mlt_service");
        char *resource = mlt_properties_get(properties, "resource");
        if (!resource || resource[0] != '<' || resource[strlen(resource) - 1] != '>')
            mlt_type = mlt_properties_get(properties, "mlt_type" );
        if (service_name)
            message = QString("[%1 %2] ").arg(mlt_type).arg(service_name);
        else
            message = QString::asprintf("[%s %p] ", mlt_type, service);
        if (resource)
            message.append(QString("\"%1\" ").arg(resource));
        message.append(QString::vasprintf(format, args));
        message.replace('\n', "");
    } else {
        message = QString::vasprintf(format, args);
        message.replace('\n', "");
    }
    cuteLogger->write(cuteLoggerLevel, __FILE__, __LINE__, "MLT",
                      cuteLogger->defaultCategory().toLatin1().constData(), message);
}

class Application : public QApplication
{
public:
    MainWindow* mainWindow;
    QTranslator qtTranslator;
    QTranslator qtBaseTranslator;
    QTranslator shotcutTranslator;
    QStringList resourceArg;
    bool isFullScreen;
    QString appDirArg;

    Application(int &argc, char **argv)
        : QApplication(argc, argv)
    {
        auto appPath = applicationDirPath();
#ifdef Q_OS_WIN
#include <winbase.h>
        SetDllDirectoryA(appPath.toLocal8Bit());
#endif
        QDir dir(appPath);
#ifdef Q_OS_MAC
        dir.cdUp();
        dir.cd("PlugIns");
        dir.cd("qt");
#else
        dir.cd("lib");
        dir.cd("qt5");
#endif
        addLibraryPath(dir.absolutePath());
        setOrganizationName("Meltytech");
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
        setOrganizationDomain("shotcut.org");
        setDesktopFileName("org.shotcut.Shotcut");
#else
        setOrganizationDomain("meltytech.com");
#endif
        setApplicationName("Shotcut");
        setApplicationVersion(SHOTCUT_VERSION);
        setAttribute(Qt::AA_UseHighDpiPixmaps);
        setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
#if defined(Q_OS_MAC)
        setAttribute(Qt::AA_DontShowIconsInMenus);
#endif

        // Process command line options.
        QCommandLineParser parser;
        parser.addHelpOption();
        parser.addVersionOption();
#ifndef Q_OS_WIN
        QCommandLineOption fullscreenOption("fullscreen",
            QCoreApplication::translate("main", "Fill the screen with the Shotcut window."));
        parser.addOption(fullscreenOption);
#endif
        QCommandLineOption noupgradeOption("noupgrade",
            QCoreApplication::translate("main", "Hide upgrade prompt and menu item."));
        parser.addOption(noupgradeOption);
        QCommandLineOption gpuOption("gpu",
            QCoreApplication::translate("main", "Use GPU processing."));
        parser.addOption(gpuOption);
        QCommandLineOption clearRecentOption("clear-recent",
            QCoreApplication::translate("main", "Clear Recent on Exit"));
        parser.addOption(clearRecentOption);
        QCommandLineOption appDataOption("appdata",
            QCoreApplication::translate("main", "The directory for app configuration and data."),
            QCoreApplication::translate("main", "directory"));
        parser.addOption(appDataOption);
        QCommandLineOption scaleOption("QT_SCALE_FACTOR",
            QCoreApplication::translate("main", "The scale factor for a high-DPI screen"),
            QCoreApplication::translate("main", "number"));
        parser.addOption(scaleOption);
        scaleOption = QCommandLineOption("QT_SCREEN_SCALE_FACTORS",
            QCoreApplication::translate("main", "A semicolon-separated list of scale factors for each screen"),
            QCoreApplication::translate("main", "list"));
        parser.addOption(scaleOption);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
        QCommandLineOption scalePolicyOption("QT_SCALE_FACTOR_ROUNDING_POLICY",
            QCoreApplication::translate("main", "How to handle a fractional display scale: %1")
                .arg("Round, Ceil, Floor, RoundPreferFloor, PassThrough"),
            QCoreApplication::translate("main", "string"), kDefaultScaleRoundPolicy);
        parser.addOption(scalePolicyOption);
#endif
        parser.addPositionalArgument("[FILE]...",
            QCoreApplication::translate("main", "Zero or more files or folders to open"));
        parser.process(arguments());
#ifdef Q_OS_WIN
        isFullScreen = false;
#else
        isFullScreen = parser.isSet(fullscreenOption);
#endif
        setProperty("noupgrade", parser.isSet(noupgradeOption));
        setProperty("clearRecent", parser.isSet(clearRecentOption));
        if (!parser.value(appDataOption).isEmpty()) {
            appDirArg = parser.value(appDataOption);
            ShotcutSettings::setAppDataForSession(appDirArg);
        }
        if (parser.isSet(gpuOption))
            Settings.setPlayerGPU(true);
        if (!parser.positionalArguments().isEmpty())
            resourceArg = parser.positionalArguments();

        // Startup logging.
        dir.setPath(Settings.appDataLocation());
        if (!dir.exists()) dir.mkpath(dir.path());
        const QString logFileName = dir.filePath("shotcut-log.txt");
        QFile::remove(logFileName);
        FileAppender* fileAppender = new FileAppender(logFileName);
        fileAppender->setFormat("[%{type:-7}] <%{function}> %{message}\n");
        cuteLogger->registerAppender(fileAppender);
#ifndef NDEBUG
        // Only log to console in dev debug builds.
        ConsoleAppender* consoleAppender = new ConsoleAppender();
        consoleAppender->setFormat(fileAppender->format());
        cuteLogger->registerAppender(consoleAppender);

        mlt_log_set_level(MLT_LOG_VERBOSE);
#else
        mlt_log_set_level(MLT_LOG_INFO);
#endif
        mlt_log_set_callback(mlt_log_handler);
        cuteLogger->logToGlobalInstance("qml", true);

        // Log some basic info.
        LOG_INFO() << "Starting Shotcut version" << SHOTCUT_VERSION;
#if defined (Q_OS_WIN)
        LOG_INFO() << "Windows version" << QSysInfo::windowsVersion();
#elif defined(Q_OS_MAC)
        LOG_INFO() << "macOS version" << QSysInfo::macVersion();
#else
        LOG_INFO() << "Linux version";
#endif
        LOG_INFO() << "number of logical cores =" << QThread::idealThreadCount();
        LOG_INFO() << "locale =" << QLocale();
        LOG_INFO() << "install dir =" << appPath;
        Settings.log();

#if defined(Q_OS_WIN)
        dir.setPath(appPath);
        if (!Settings.playerGPU() && Settings.drawMethod() == Qt::AA_UseSoftwareOpenGL) {
            if (QFile::exists(dir.filePath("opengl32sw.dll"))) {
                if (!QFile::copy(dir.filePath("opengl32sw.dll"), dir.filePath("opengl32.dll"))) {
                    LOG_WARNING() << "Failed to copy opengl32sw.dll as opengl32.dll";
                }
            }
        } else if (QFile::exists(dir.filePath("opengl32.dll"))) {
            if (!QFile::remove(dir.filePath("opengl32.dll"))) {
                LOG_ERROR() << "Failed to remove opengl32.dll";
            }
        }
        if (Settings.playerGPU()) {
            QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
        } else if (Settings.drawMethod() >= Qt::AA_UseDesktopOpenGL &&
                   Settings.drawMethod() <= Qt::AA_UseSoftwareOpenGL) {
            QCoreApplication::setAttribute(Qt::ApplicationAttribute(Settings.drawMethod()));
        }
#elif !defined(Q_OS_MAC)
        if (Settings.drawMethod() == Qt::AA_UseSoftwareOpenGL && !Settings.playerGPU()) {
            ::qputenv("LIBGL_ALWAYS_SOFTWARE", "1");
        }
#endif
        // Load translations
        QString locale = Settings.language();
        dir.setPath(appPath);
    #if defined(Q_OS_MAC)
        dir.cdUp();
        dir.cd("Resources");
        dir.cd("translations");
    #elif defined(Q_OS_WIN)
        dir.cd("share");
        dir.cd("translations");
    #else
        dir.cdUp();
        dir.cd("share");
        dir.cd("shotcut");
        dir.cd("translations");
    #endif
        if (locale.startsWith("pt_"))
            locale = "pt";
        else if (locale.startsWith("en_"))
            locale = "en";
        if (qtTranslator.load("qt_" + locale, QLibraryInfo::location(QLibraryInfo::TranslationsPath)))
            installTranslator(&qtTranslator);
        else if (qtTranslator.load("qt_" + locale, dir.absolutePath()))
            installTranslator(&qtTranslator);
        if (qtBaseTranslator.load("qtbase_" + locale, QLibraryInfo::location(QLibraryInfo::TranslationsPath)))
            installTranslator(&qtBaseTranslator);
        else if (qtBaseTranslator.load("qtbase_" + locale, dir.absolutePath()))
            installTranslator(&qtBaseTranslator);
        if (shotcutTranslator.load("shotcut_" + Settings.language(), dir.absolutePath()))
            installTranslator(&shotcutTranslator);
    }

    ~Application()
    {
        delete mainWindow;
        LOG_DEBUG() << "exiting";
    }

protected:
    bool event(QEvent *event) {
        if (event->type() == QEvent::FileOpen) {
            QFileOpenEvent *openEvent = static_cast<QFileOpenEvent*>(event);
            resourceArg << openEvent->file();
            return true;
        }
        else return QApplication::event(event);
    }
};

int main(int argc, char **argv)
{
#if defined(Q_OS_WIN) && defined(QT_DEBUG)
    ExcHndlInit();
#endif
#ifndef QT_DEBUG
    ::qputenv("QT_LOGGING_RULES", "*.warning=false");
#endif
#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    for (int i = 1; i + 1 < argc; i++) {
        if (!::qstrcmp("--QT_SCALE_FACTOR", argv[i]) || !::qstrcmp("--QT_SCREEN_SCALE_FACTORS", argv[i])) {
            QByteArray value(argv[i + 1]);
            ::qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "0");
            ::qputenv(value.contains(';')? "QT_SCREEN_SCALE_FACTORS" : "QT_SCALE_FACTOR", value);
            break;
        }
    }
#endif
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
    QByteArray value(kDefaultScaleRoundPolicy);
    for (int i = 1; i + 1 < argc; i++) {
        if (!::qstrcmp("--QT_SCALE_FACTOR_ROUNDING_POLICY", argv[i])) {
            value = argv[i + 1];
            break;
        }
    }
    if (!::qEnvironmentVariableIsSet("QT_SCALE_FACTOR_ROUNDING_POLICY")) {
        ::qputenv("QT_SCALE_FACTOR_ROUNDING_POLICY", value);
    }
#endif
#ifdef Q_OS_MAC
#if (QT_VERSION < QT_VERSION_CHECK(5, 13, 0))
    // Fix launch on Big Sur macOS 11.0
    // We can probably remove this when upgrade to Qt 5.15 and update build environment.
    // see https://bugreports.qt.io/browse/QTBUG-87014
    ::qputenv("QT_MAC_WANTS_LAYER", "1");
#endif
    // Launcher and Spotlight on macOS are not setting this environment
    // variable needed by setlocale() as used by MLT.
    if (QProcessEnvironment::systemEnvironment().value(MLT_LC_NAME).isEmpty()) {
        qputenv(MLT_LC_NAME, QLocale().name().toUtf8());

        QLocale localeByName(QLocale(QLocale().language(), QLocale().script(), QLocale().country()));
        if (QLocale().decimalPoint() != localeByName.decimalPoint()) {
            // If region's numeric format does not match the language's, then we run
            // into problems because we told MLT and libc to use a different numeric
            // locale than actually in use by Qt because it is unable to give numeric
            // locale as a set of ISO-639 codes.
            QLocale::setDefault(localeByName);
            qputenv("LANG", QLocale().name().toUtf8());
        }
    }
    removeMacosTabBar();
#endif

    Application a(argc, argv);
    QSplashScreen splash(QPixmap(":/icons/shotcut-logo-320x320.png"));

    // Expire old items from the qmlcache
    splash.showMessage(QCoreApplication::translate("main", "Expiring cache..."), Qt::AlignRight | Qt::AlignVCenter);
    splash.show();
    a.processEvents();
    auto dir = QDir(QStandardPaths::standardLocations(QStandardPaths::CacheLocation).constFirst());
    if (dir.exists() && dir.cd("qmlcache")) {
        auto ls = dir.entryList(QDir::Files | QDir::Readable | QDir::NoDotAndDotDot, QDir::Time);
        if (qMax(0, ls.size() - kMaxCacheCount) > 0) {
            LOG_INFO() << "removing" << qMax(0, ls.size() - kMaxCacheCount) << "from" << dir.path();
        }
        for (int i = kMaxCacheCount; i < ls.size(); i++) {
            QString filePath = dir.filePath(ls[i]);
            if (!QFile::remove(filePath)) {
                LOG_WARNING() << "failed to delete" << filePath;
            }
            if (i % 1000 == 0) {
                a.processEvents();
            }
        }
    }

    splash.showMessage(QCoreApplication::translate("main", "Loading plugins..."), Qt::AlignRight | Qt::AlignVCenter);
    a.processEvents();

    QQuickStyle::setStyle("Fusion");
    a.setProperty("system-style", a.style()->objectName());
    MainWindow::changeTheme(Settings.theme());

    a.mainWindow = &MAIN;
    if (!a.appDirArg.isEmpty())
        a.mainWindow->hideSetDataDirectory();
#ifdef Q_OS_WIN
    a.mainWindow->setProperty("windowOpacity", 0.0);
#endif
    a.mainWindow->show();
    a.processEvents();
    a.mainWindow->setFullScreen(a.isFullScreen);
    splash.finish(a.mainWindow);

    if (!a.resourceArg.isEmpty())
        a.mainWindow->openMultiple(a.resourceArg);
    else
        a.mainWindow->open(a.mainWindow->untitledFileName());

    int result = a.exec();

    if (EXIT_RESTART == result) {
        LOG_DEBUG() << "restarting app";
#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC)
        ::qputenv("LIBGL_ALWAYS_SOFTWARE", 
            Settings.drawMethod() == Qt::AA_UseSoftwareOpenGL && !Settings.playerGPU()
            ? "1" : "0");
#endif
        QProcess* restart = new QProcess;
        QStringList args = a.arguments();
        if (!args.isEmpty())
            args.removeFirst();
        restart->start(a.applicationFilePath(), args, QIODevice::NotOpen);
        result = EXIT_SUCCESS;
    }
    return result;
}
