/****************************************************************************
**
** Copyright (C) 2023 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Design Viewer of the Qt Toolkit.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/

#include "backend.h"

#include <QDesktopServices>
#include <QNetworkInterface>

#ifdef Q_OS_ANDROID
#include <QJniObject>
#endif

#include "logger.h"

#ifdef QT_DEBUG
#define buildType "Debug"
#else
#define buildType "Release"
#endif

#define FLAG_KEEP_SCREEN_ON 0x00000080

Backend::Backend(QObject *parent)
    : QObject(parent)
{
    // register the `qtdesignstudio` url handler to the Android system
    QDesktopServices::setUrlHandler("qtdesignstudio", this, "parseDesignViewerUrl");

    if (m_settings.deviceUuid().isEmpty()) {
        qDebug() << "Device UUID not found. Generating a new one.";
        m_settings.setDeviceUuid(QUuid::createUuid().toString(QUuid::WithoutBraces));
    }

    m_dsManagerThread.setParent(this);
    connect(&m_dsManagerThread, &QThread::started, this, &Backend::initDsManager);
    m_dsManagerThread.start();

    m_projectManagerThread.setParent(this);
    connect(&m_projectManagerThread, &QThread::started, this, &Backend::initProjectManager);
    m_projectManagerThread.start();

    connect(&Logger::instance(), &Logger::logMessage, this, [this](QtMsgType type, QString &msg) {
        // if we have any active project running, then reroute
        // all the logs to the dsmanager with the last project sender id
        if (m_projectManager && !m_lastSessionId.isEmpty()
            && m_lastSessionId == m_projectManager->sessionId()) {
            QMetaObject::invokeMethod(m_dsManager.get(),
                                      "sendProjectLogs",
                                      Qt::QueuedConnection,
                                      Q_ARG(QString, m_lastProjectSenderId),
                                      Q_ARG(QString, msg));
        }
    });

    const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();

    qDebug() << "Qt Design Viewer";
    qDebug() << "System information:";
    qDebug() << "-- Qt version: " << QT_VERSION_STR;
    qDebug() << "-- OpenSSL support: " << QVariant(QSslSocket::supportsSsl()).toString();
    qDebug() << "-- Screen height: " << QString::number(screenGeometry.height());
    qDebug() << "-- Screen width: " << QString::number(screenGeometry.width());
    qDebug() << "-- OS: " << QSysInfo::prettyProductName();
    qDebug() << "-- OS version: " << QSysInfo::productVersion();
    qDebug() << "-- Architecture: " << QSysInfo::currentCpuArchitecture();
    qDebug() << "-- Boot ID: " << QSysInfo::bootUniqueId();
    qDebug() << "-- Kernel type: " << QSysInfo::kernelType();
    qDebug() << "-- Kernel version: " << QSysInfo::kernelVersion();
    qDebug() << "-- Machine unique ID: " << QSysInfo::machineUniqueId();
    qDebug() << "-- Product type: " << QSysInfo::productType();
    qDebug() << "-- Product version: " << QSysInfo::productVersion();
    qDebug() << "-- Build ABI: " << QSysInfo::buildAbi();
    qDebug() << "-- Build CPU architecture: " << QSysInfo::buildCpuArchitecture();
    qDebug() << "-- Device unique ID: " << m_settings.deviceUuid();

    setAndroidScreenOn(keepScreenOn());
}

Backend::~Backend()
{
    m_dsManagerThread.quit();
    m_projectManagerThread.quit();
    m_dsManagerThread.wait();
    m_projectManagerThread.wait();
}

QString Backend::buildInfo() const
{
    // clang-format off
    return {
        QCoreApplication::applicationVersion() +
        "\n"+ CMAKE_VAR_GIT_VERSION +
        "\nQt " + QT_VERSION_STR + " - " + buildType + " Build" +
        "\nQt Quick Components " + CMAKE_VAR_QT_QUICK_COMPONENTS_VERSION +
        "\nZXing-Cpp: " + CMAKE_VAR_ZXING_VERSION +
        "\nOpenSSL support: " + QVariant(QSslSocket::supportsSsl()).toString()};
    // clang-format on
}

void Backend::updatePopupText(const QString &text, int timeout)
{
    emit popupOpen();
    emit popupChangeText(text, timeout);
    emit popupProgressIndeterminateChanged(true);
    QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
}

void Backend::updatePopupProgress(const int progress)
{
    emit popupOpen();
    emit popupProgressIndeterminateChanged(false);
    emit popupChangeProgress(progress);
    QEventLoop().processEvents(QEventLoop::AllEvents, 1000);
}

void Backend::initProjectManager()
{
    m_projectManager.reset(new ProjectManager(this));

    connect(m_projectManager.get(),
            &ProjectManager::closingProject,
            this,
            [this](const QString &sessionId) {
                // if seesion ids are same, it's most likely the project was running
                // from the leftover session (DS connected, project started,
                // DS disconnected without stopping the project)
                // so we'll stop the project and do not send any signals to the DS
                if (sessionId != m_lastSessionId)
                    return;

                QMetaObject::invokeMethod(m_dsManager.get(),
                                          "sendProjectStopped",
                                          Qt::QueuedConnection,
                                          Q_ARG(QString, m_lastProjectSenderId));
            });
}

void Backend::initDsManager()
{
    qDebug() << "Design Studio Manager thread started. Initializing Design Studio Manager";
    m_dsManager.reset(new DesignStudioManager(m_settings.deviceUuid(), this));

    connect(m_dsManager.get(), &DesignStudioManager::projectReceived, this, &Backend::runProject);

    connect(m_dsManager.get(),
            &DesignStudioManager::designStudioConnected,
            this,
            [this](const QString &id, const QString &ipAddr) { emit connectedChanged(true); });
    connect(m_dsManager.get(),
            &DesignStudioManager::designStudioDisconnected,
            this,
            [this](const QString &id, const QString &ipAddr) {
                if (id == m_lastProjectSenderId) {
                    QMetaObject::invokeMethod(m_projectManager.get(), "stopProject");
                }
                emit popupClose();
            });

    connect(m_dsManager.get(), &DesignStudioManager::allDesignStudiosDisconnected, this, [this] {
        qDebug() << "All Design Studios disconnected";
        emit connectedChanged(false);
    });

    connect(m_dsManager.get(),
            &DesignStudioManager::projectIncoming,
            this,
            [this](const QString &id, const int projectSize) {
                qDebug() << "Project incoming with size" << projectSize;
                emit updatePopupText("Receiving project...");
                // we'll use this to notify the correct DS when the project started/stopped
                m_lastProjectSenderId = id;
                m_lastSessionId = QUuid::createUuid().toString(QUuid::WithoutBraces);
                QMetaObject::invokeMethod(m_projectManager.get(),
                                          "stopProject",
                                          Qt::QueuedConnection);

            });

    connect(m_dsManager.get(),
            &DesignStudioManager::projectIncomingProgress,
            this,
            [this](const QString &id, const int percentage) { updatePopupProgress(percentage); });

    connect(m_dsManager.get(),
            &DesignStudioManager::projectStopRequested,
            this,
            [this](const QString &id) {
                qDebug() << "Project stop requested";
                emit popupClose();
                QMetaObject::invokeMethod(m_projectManager.get(),
                                          "stopProject",
                                          Qt::QueuedConnection);
                QMetaObject::invokeMethod(m_dsManager.get(),
                                          "sendProjectStopped",
                                          Qt::QueuedConnection,
                                          Q_ARG(QString, m_lastProjectSenderId));
            });

    m_dsManager->init();
    qDebug() << "Design Studio Manager initialized";
}

void Backend::connectDesignStudio(const QString &ipAddr)
{
    QMetaObject::invokeMethod(m_dsManager.get(),
                              "initDesignStudio",
                              Q_ARG(QString, ipAddr),
                              Q_ARG(QString, ""));
    emit updatePopupText("Connecting in the background...", 1500);
}

void Backend::runProject(const QString &id, const QByteArray &projectData)
{
    emit updatePopupText("Running project...");

    QTimer::singleShot(1000, [this, id, projectData] {
        bool retVal;
        QMetaObject::invokeMethod(m_projectManager.get(),
                                  "runProject",
                                  Q_RETURN_ARG(bool, retVal),
                                  Q_ARG(QByteArray, projectData),
                                  Q_ARG(bool, autoScaleProject()),
                                  Q_ARG(QString, m_lastSessionId));

        if (!retVal)
            QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectStopped", Q_ARG(QString, id));
        else
            QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectRunning", Q_ARG(QString, id));

        emit popupClose();
    });
}

void Backend::scanQrCode()
{
    m_qrScanner.reset(new QrScanner);
    connect(m_qrScanner.get(), &QrScanner::qrCodeScanned, this, [&](const QString &qrCode) {
        qDebug() << "QR code scanned:" << qrCode;
        m_qrScanner.reset();
        parseDesignViewerUrl(QUrl(qrCode));
    });

    connect(m_qrScanner.get(), &QrScanner::windowClosed, this, [&]() { m_qrScanner.reset(); });

    m_qrScanner->scanQrCode();
}

void Backend::parseDesignViewerUrl(const QUrl &url)
{
    if (url.scheme() != "qtdesignstudio") {
        qWarning() << "Unknown QR code format";
        qWarning() << "URL:" << url.toString();
        return;
    }

    if (url.host().isEmpty()) {
        qWarning() << "No Design Studio IP address found in the QR code. URL:" << url.toString();
        return;
    }

    connectDesignStudio(url.host());
}

void Backend::popupInterrupted()
{
    qDebug() << "Popup closed prematurely. Interrupting active downloads (if any)";
}

bool Backend::autoScaleProject() const
{
    return m_settings.autoScaleProject();
}

void Backend::setAutoScaleProject(bool autoScaleProject)
{
    m_settings.setAutoScaleProject(autoScaleProject);
}

bool Backend::keepScreenOn() const
{
    return m_settings.keepScreenOn();
}

void Backend::setKeepScreenOn(bool keepScreenOn)
{
    m_settings.setKeepScreenOn(keepScreenOn);
    setAndroidScreenOn(keepScreenOn);
}

void Backend::setAndroidScreenOn(bool on)
{
#ifdef Q_OS_ANDROID
    QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() {
        QJniObject activity = QNativeInterface::QAndroidApplication::context();
        QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;");
        on ? window.callMethod<void>("addFlags", "(I)V", FLAG_KEEP_SCREEN_ON)
           : window.callMethod<void>("clearFlags", "(I)V", FLAG_KEEP_SCREEN_ON);
    });
#else
    Q_UNUSED(on);
#endif
}

QString Backend::lastDesignStudioIp() const
{
    return m_dsManager ? m_dsManager->getDesignStudioIp({}) : QString();
}

QJsonArray Backend::getIpAddresses() const
{
    QNetworkInterface networkInterface;
    QJsonArray ipAddresses;

    for (const auto &interface : networkInterface.allInterfaces()) {
        for (const auto &entry : interface.addressEntries()) {
            if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol && !entry.ip().isLoopback()) {
                ipAddresses.append(
                    QJsonObject{{"interface", interface.name()}, {"ip", entry.ip().toString()}});
            }
        }
    }

    return ipAddresses;
}